root/trunk/src/semitwist/cmdlineparser.d

Revision 240, 17.3 kB (checked in by Abscissa, 6 months ago)

Fixed for 64bit

  • Property svn:eol-style set to native
Line 
1 // SemiTwist Library
2 // Written in the D programming language.
3
4 module semitwist.cmdlineparser;
5
6 import std.math;
7 import std.string;
8 import std.conv;
9 import std.stdio;
10
11 public import semitwist.refbox;
12 import semitwist.util.all;
13
14 //TODO: This module's API needs a serious overhaul.
15
16 //TODO: Add "switch A implies switches B and C"
17 //TODO: Add in some good ideas from the cmd parser in tango scrapple
18
19 //TODO: Convert the following sample code into an actual sample app
20 /**
21 ----- THIS IS PROBABLY OUTDATED -----
22 Usage:
23
24 void main(string[] args)
25 {
26     bool help;
27     bool detailhelp;
28     int myInt = 2; // Default value == 2
29     bool myBool;   // Default value == bool.init (ie, false)
30     string myStr;  // Default value == (string).init (ie, "")
31
32     auto cmd = new CmdLineParser();
33     mixin(defineArg!(cmd, help,       "help",       "Displays a help summary and exits" ));
34     mixin(defineArg!(cmd, detailhelp, "detailhelp", "Displays a detailed help message and exits" ));
35     mixin(defineArg!(cmd, myInt,  "num",  "An integer"));
36     mixin(defineArg!(cmd, myBool, "flag", "A flag"));
37     mixin(defineArg!(cmd, myStr,  "str",  "A string"));
38     
39     if(!cmd.parse(args) || help)
40     {
41         Stdout.format("{}", cmd.getUsage());
42         return;
43     }
44     if(detailhelp)
45     {
46         Stdout.format("{}", cmd.getDetailedUsage());
47         return;
48     }
49
50     Stdout.formatln("num:  {}", myInt);
51     Stdout.formatln("flag: {}", myBool);
52     Stdout.formatln("str:  {}", myStr);
53 }
54
55 Sample Command Lines (All these are equivalent):
56 > myApp.exe /num:5 /flag /str:blah
57 > myApp.exe -flag:true -str:blah -num=5
58 > myApp.exe --num=5 /flag+ "-str:blah"
59 num:  5
60 flag: true
61 str:  blah
62
63 > myApp.exe "/str:Hello World"
64 num:  2
65 flag: false
66 str:  Hello World
67
68 > myApp.exe /foo
69 Unknown switch: "/foo"
70 Switches: (prefixes can be '/', '-' or '--')
71   -help               Displays a help summary and exits
72   -detailhelp         Displays a detailed help message and exits
73   -num:<int>          An integer (default: 2)
74   -flag               A flag
75   -str:<string>       A string
76  
77 */
78
79 enum ArgFlag
80 {
81     Optional   = 0b0000_0000,
82     Required   = 0b0000_0001,
83     //Unique = 0b0000_0100,
84     ToLower    = 0b0000_1000, // If arg is string, the value gets converted to all lower-case (for case-insensitivity)
85     Advanced   = 0b0001_0000,
86 }
87
88 template defineArg(alias cmdLineParser, string name, alias var, int flags = cast(int)ArgFlag.Optional, string desc = "")
89 //template defineArg(alias cmdLineParser, string name, alias var, ArgFlag flags = ArgFlag.Optional, string desc = "")
90 {
91     //TODO: Is there a better way to do this? Ex. "static if(typeof(var) !contained_in listOfSupportedTypes)"
92     static if(!is(typeof(var) == int   ) && !is(typeof(var) == int[]   ) &&
93               !is(typeof(var) == bool  ) && !is(typeof(var) == bool[]  ) &&
94               !is(typeof(var) == string) && !is(typeof(var) == string[]) )
95     {
96         static assert(false, `Attempted to pass variable '`~var.stringof~`' of type '`~typeof(var).stringof~`' to defineArg's 'var' param.`"\n"
97                              `(The type must be one of, or an array of, one of the following: 'int' 'bool' 'string')`);
98     }
99     else
100     {
101         enum defineArg = "\n"~
102             "auto _cmdarg_refbox_"~name~" = new "~nameof!(RefBox)~"!("~typeof(var).stringof~")(&"~var.stringof~");\n"~
103             "auto _cmdarg_"~name~" = new Arg(_cmdarg_refbox_"~name~`, "`~name~`", `~desc.stringof~`);`~"\n"~
104             cmdLineParser.stringof~".addArg(_cmdarg_"~name~", cast(ArgFlag)("~flags.stringof~"));\n";
105     }
106 }
107
108 template setArgAllowableValues(string name, allowableValues...)
109 {
110     enum setArgAllowableValues =
111         typeof(allowableValues[0]).stringof~"[] _cmdarg_allowablevals_"~name~";\n"
112         ~_setArgAllowableValues!(name, allowableValues)
113         ~"_cmdarg_"~name~".setAllowableValues(_cmdarg_allowablevals_"~name~");\n";
114 }
115
116 private template _setArgAllowableValues(string name, allowableValues...)
117 {
118     static if(allowableValues.length == 0)
119         enum _setArgAllowableValues = "";
120     else
121         enum _setArgAllowableValues =
122             "_cmdarg_allowablevals_"~name~" ~= "~allowableValues[0].stringof~";\n"
123             ~ _setArgAllowableValues!(name, allowableValues[1..$]);
124 }
125
126 //TODO? Add float, double, byte, short, long, and unsigned of each.
127 //TODO: For numeric types, make sure provided values can fit in the type. (Using "to!()"?)
128 //TODO: Think about way to (or the need to) prevent adding
129 //      the same Arg instance to multiple Parsers.
130
131 class Arg
132 {
133     string name;
134     string altName;
135     string desc;
136
137     bool isSwitchless  = false;
138     bool isRequired    = false;
139     bool arrayUnique   = false;
140     bool toLower       = false;
141     bool isAdvanced    = false;
142    
143     private Object value;
144     private Object defaultValue;
145     private Object[] allowableValues;
146    
147     bool isSet = false;
148    
149     this(Object value, string name, string desc="")
150     {
151         mixin(initMember("value", "name", "desc"));
152         ensureValid();
153     }
154    
155     private void genDefaultValue()
156     {
157         if(!isRequired)
158         {
159             mixin(dupRefBox!(value, "val", defaultValue));
160         }
161     }
162    
163     // Note: AllowableValues are ignored for bool and bool[]
164     private void setAllowableValues(T)(T[] allowableValues)
165     {
166         this.allowableValues.length = 0;
167         foreach(T val; allowableValues)
168         {
169             auto box = new RefBox!(T)();
170             box = val;
171             this.allowableValues ~= box;
172         }
173     }
174    
175     void ensureValid()
176     {
177         //TODO: arrayMultiple and arrayUnique cannot both be set
178         //TODO: ensure each of allowableValues is the same type as value
179         //TODO: enforce allowableValues on defaultValue
180         //TODO: reflect allowableValues in generated help
181        
182         if(!isKnownRefBox!(value))
183         {
184             throw new Exception("Param to Arg contructor must be "~RefBox.stringof~", where T is int, bool or string or an array of such types.");
185         }
186        
187         void ensureValidName(string name)
188         {
189             if(!CmdLineParser.isValidArgName(name))
190                 throw new Exception(`Tried to define an invalid arg name: "%s". Arg names must be "[a-zA-Z0-9_?]*"`.format(name));
191         }
192         ensureValidName(name);
193         ensureValidName(altName);
194     }
195 }
196
197 class CmdLineParser
198 {
199     private Arg[] args;
200     private Arg[string] argLookup;
201    
202     private bool switchlessArgExists=false;
203     private size_t switchlessArg;
204    
205     mixin(getter!(bool, "success"));
206     mixin(getter!(string, "errorMsg"));
207
208     private enum Prefix
209     {
210         Invalid, DoubleDash, SingleDash, Slash
211     }
212    
213     enum ParseArgResult
214     {
215         Done, NotFound, Error
216     }
217    
218     static bool isValidArgName(string name)
219     {
220         foreach(char c; name)
221         {
222             if(!inPattern(c, "a-zA-Z0-9") && c != '_' && c != '?')
223                 return false;
224         }
225         return true;
226     }
227    
228     private void ensureValid()
229     {
230         foreach(Arg arg; args)
231         {
232             arg.ensureValid();
233         }
234     }
235    
236     private void populateLookup()
237     {
238         foreach(Arg arg; args)
239         {
240             addToArgLookup(arg.name, arg);
241            
242             if(arg.altName != "")
243                 addToArgLookup(arg.altName, arg);
244         }
245     }
246
247     private void genDefaultValues()
248     {
249         foreach(Arg arg; args)
250         {
251             arg.genDefaultValue();
252         }
253     }
254
255     public void addArg(Arg arg, ArgFlag flags = ArgFlag.Optional)
256     {
257         args ~= arg;
258
259         bool isSwitchless = arg.name == "";
260         bool isRequired   = ((flags & ArgFlag.Required)   != 0);
261         bool toLower      = ((flags & ArgFlag.ToLower)    != 0);
262         bool isAdvanced   = ((flags & ArgFlag.Advanced)   != 0);
263        
264         mixin(initMemberTo("arg", "isRequired", "toLower", "isAdvanced"));
265        
266         if(isSwitchless)
267         {
268             if(switchlessArgExists)
269                 args[switchlessArg].isSwitchless = false;
270            
271             switchlessArgExists = true;
272             switchlessArg = args.length-1;
273             arg.isSwitchless = true;
274         }
275     }
276    
277     private void addToArgLookup(string name, Arg argDef)
278     {
279         if(name in argLookup)
280             throw new Exception(`Argument name "%s" defined more than once.`.format(name));
281
282         argLookup[name] = argDef;
283     }
284    
285     private void splitArg(string fullArg, out Prefix prefix, out string name, out string suffix)
286     {
287         string argNoPrefix;
288
289         // Get prefix
290         if(fullArg.length > 2 && fullArg[0..2] == "--")
291         {
292             argNoPrefix = fullArg[2..$];
293             prefix = Prefix.DoubleDash;
294         }
295         else if(fullArg.length > 1)
296         {
297             argNoPrefix = fullArg[1..$];
298            
299             if(fullArg[0] == '-')
300                 prefix = Prefix.SingleDash;
301             else if(fullArg[0] == '/')
302                 prefix = Prefix.Slash;
303             else
304             {
305                 prefix = Prefix.Invalid;
306                 argNoPrefix = fullArg;
307             }
308         }
309        
310         // Get suffix and arg name
311         version(GNU)
312         {
313             auto tmp = [locate(argNoPrefix, ':'), locate(argNoPrefix, '+')];
314             tmp ~= locate(argNoPrefix, '-');
315             auto suffixIndex = reduce!"a<b?a:b"(tmp);
316         }
317         else
318         {
319             auto suffixIndex = reduce!"a<b?a:b"( [
320                 locate(argNoPrefix, ':'),
321                 locate(argNoPrefix, '+'),
322                 locate(argNoPrefix, '-')
323             ] );
324         }
325         name = argNoPrefix[0..suffixIndex];
326         suffix = suffixIndex < argNoPrefix.length ?
327                  argNoPrefix[suffixIndex..$] : "";
328     }
329    
330     //TODO: Detect and error when numerical arg is passed an out-of-range value
331     private ParseArgResult parseArg(string cmdArg, string cmdName, string suffix)
332     {
333         ParseArgResult ret = ParseArgResult.Error;
334
335         void HandleMalformedArgument()
336         {
337             _errorMsg ~= `Invalid value: "%s"`.formatln(cmdArg);
338             ret = ParseArgResult.Error;
339         }
340        
341         if(cmdName in argLookup)
342         {
343             auto argDef = argLookup[cmdName];
344            
345             // For some reason, unbox can't see Arg's private member "value"
346             auto argDefValue = argDef.value;
347             mixin(unbox!(argDefValue, "val"));
348
349             ret = ParseArgResult.Done;
350             if(argDef.isSet && (valAsBool || valAsStr || valAsInt))
351             {
352                 _errorMsg ~= `Switch given twice: "%s"`.formatln(cmdArg);
353                 ret = ParseArgResult.Error;
354             }
355             else if(valAsBool || valAsBools)
356             {
357                 bool val;
358                 bool isMalformed=false;
359                 switch(suffix)
360                 {
361                 case "":
362                 case "+":
363                 case ":+":
364                 case ":true":
365                     val = true;
366                     break;
367                 case "-":
368                 case ":-":
369                 case ":false":
370                     val = false;
371                     break;
372                 default:
373                     HandleMalformedArgument();
374                     isMalformed = true;
375                     break;
376                 }
377                
378                 if(!isMalformed)
379                 {
380                     if(valAsBool)
381                         valAsBool = val;
382                     else
383                         valAsBools = valAsBools() ~ val;
384                 }
385             }
386             else if(valAsStr || valAsStrs)
387             {
388                 string val;
389                 if(suffix.length > 1 && suffix[0] == ':')
390                 {
391                     val = strip(suffix[1..$]);
392
393                     if(argDef.toLower)
394                         val = val.tolower();
395                    
396                     //TODO: DRY this
397                     if(argDef.allowableValues.length > 0)
398                     {
399                         bool matchFound=false;
400                         foreach(Object allowedObj; argDef.allowableValues)
401                         {
402                             mixin(unbox!(allowedObj, "allowedVal"));
403                             if(val == allowedValAsStr)
404                             {
405                                 matchFound = true;
406                                 break;
407                             }
408                         }
409                         if(!matchFound)
410                             HandleMalformedArgument();
411                     }
412
413                     if(valAsStr)
414                         valAsStr = val;
415                     else
416                         valAsStrs = valAsStrs() ~ val;
417                 }
418                 else
419                     HandleMalformedArgument();
420             }
421             else if(valAsInt || valAsInts)
422             {
423                 int val;
424                 size_t parseAte;
425                 if(suffix.length > 1 && suffix[0] == ':')
426                 {
427                     string trimmedSuffix = strip(suffix[1..$]);
428                     auto copyTrimmedSuffix = trimmedSuffix;
429                     val = std.conv.parse!int(copyTrimmedSuffix);
430                     parseAte = trimmedSuffix.length - copyTrimmedSuffix.length;
431                     //val = cast(int)convInt.parse(trimmedSuffix, 0, &parseAte);
432                     if(parseAte == trimmedSuffix.length)
433                     {
434                         //TODO: DRY this
435                         if(argDef.allowableValues.length > 0)
436                         {
437                             bool matchFound=false;
438                             foreach(Object allowedObj; argDef.allowableValues)
439                             {
440                                 mixin(unbox!(allowedObj, "allowedVal"));
441                                 if(val == allowedValAsInt)
442                                 {
443                                     matchFound = true;
444                                     break;
445                                 }
446                             }
447                             if(!matchFound)
448                                 HandleMalformedArgument();
449                         }
450
451                         if(valAsInt)
452                             valAsInt = val;
453                         else
454                             valAsInts = valAsInts() ~ val;
455                     }
456                     else
457                         HandleMalformedArgument();
458                 }
459                 else
460                     HandleMalformedArgument();
461             }
462             else
463                 throw new Exception("Internal Error: Failed to process an Arg.value type that hasn't been set as unsupported.");
464
465             argDef.isSet = true;
466         }
467         else
468         {
469             _errorMsg ~= `Unknown switch: "%s"`.formatln(cmdArg);
470             ret = ParseArgResult.NotFound;
471         }
472        
473         return ret;
474     }
475
476     //TODO: response file
477
478     public bool parse(string[] args)
479     {
480         bool error=false;
481        
482         ensureValid();
483         populateLookup();
484         genDefaultValues();
485        
486         foreach(string argStr; args[1..$])
487         {
488             string suffix;
489             string argName;
490             Prefix prefix;
491            
492             splitArg(argStr, prefix, argName, suffix);
493             if(prefix == Prefix.Invalid)
494             {
495                 if(switchlessArgExists)
496                 {
497                     argName = this.args[switchlessArg].name;
498                     suffix = ":"~argStr;
499                 }
500                 else
501                 {
502                     _errorMsg ~= `Unexpected value: "%s"`.formatln(argStr);
503                     error = true;
504                     continue;
505                 }
506             }
507             //mixin(traceVal!("argStr ", "prefix ", "argName", "suffix "));
508            
509             auto result = parseArg(argStr, argName, suffix);
510             switch(result)
511             {
512             case ParseArgResult.Done:
513                 continue;
514                
515             case ParseArgResult.Error:
516             case ParseArgResult.NotFound:
517                 error = true;
518                 break;
519                
520             default:
521                 throw new Exception("Unexpected ParseArgResult: (%s)".format(result));
522             }
523         }
524        
525         if(!verify())
526             error = true;
527        
528         _success = !error;
529         return _success;
530     }
531    
532     private bool verify()
533     {
534         bool error=false;
535        
536         foreach(Arg arg; this.args)
537         {
538             if(arg.isRequired && !arg.isSet)
539             {
540                 _errorMsg ~=
541                     `Missing switch: %s (%s)`
542                         .formatln(
543                             arg.name=="" ? "<"~getArgTypeName(arg)~">" : arg.name,
544                             arg.desc
545                         );
546                 error = true;
547             }
548         }
549        
550         return !error;
551     }
552    
553     //TODO: Make function to get the maximum length of the arg names
554
555     private string switchTypesMsg =
556 `Switch types:
557   flag (default):
558     Set s to true: -s -s+ -s:true
559     Set s to false: -s- -s:false
560     Default value: false (unless otherwise noted)
561
562   text:
563     Set s to "Hello": -s:Hello
564     Default value: "" (unless otherwise noted)
565     Case-sensitive unless otherwise noted.
566
567   num:
568     Set s to 3: -s:3
569     Default value: 0 (unless otherwise noted)
570  
571   If "[]" appears at the end of the type,
572   this means multiple values are accepted.
573   Example:
574     -s:<text[]>: -s:file1 -s:file2 -s:anotherfile
575 `;
576
577     string getArgTypeName(Arg arg)
578     {
579         string typeName = getRefBoxTypeName(arg.value);
580         return
581             (typeName == "string"  )? "text"   :
582             (typeName == "string[]")? "text[]" :
583             (typeName == "char[]"  )? "text"   :
584             (typeName == "char[][]")? "text[]" :
585             (typeName == "bool"    )? "flag"   :
586             (typeName == "bool[]"  )? "flag[]" :
587             (typeName == "int"     )? "num"    :
588             (typeName == "int[]"   )? "num[]"  :
589             typeName;
590     }
591    
592     //TODO: Fix word wrapping
593     string getUsage(int nameColumnWidth=20)
594     {
595         string ret;
596         string indent = "  ";
597         string basicArgStr;
598         string advancedArgStr;
599        
600         ret ~=
601             "Switches:\n"~
602             "(Prefixes can be '/', '-' or '--')\n"~
603             "('[]' means multiple switches are accepted)\n"; //TODO: Only show this line if such a switch exists
604
605         foreach(Arg arg; args)
606         {
607             string* argStr = arg.isAdvanced? &advancedArgStr : &basicArgStr;
608            
609             // For some reason, unbox can't see Arg's private member "defaultValue"
610             auto argDefaultValue = arg.defaultValue;
611             mixin(unbox!(argDefaultValue, "val"));
612
613             string defaultVal;
614             if(valAsInt)
615                 defaultVal = "%s".format(valAsInt());
616             else if(valAsBool)
617                 defaultVal = valAsBool() ? "true" : "";
618             else if(valAsStr)
619                 defaultVal = valAsStr() == "" ? "" : `"%s"`.format(valAsStr());
620            
621             string defaultValStr = defaultVal == "" ?
622                 "" : " (default: %s)".format(defaultVal);
623                
624             string requiredStr = arg.isRequired ?
625                 "(Required) " : "";
626            
627             string argType = "<"~getArgTypeName(arg)~">";
628             string argSuffix = valAsBool ? "" : (":"~argType);
629
630             string argName;
631             if(arg.name=="")
632                 argName = argType;
633             else
634                 argName = "-"~arg.name~argSuffix;
635             if(arg.altName != "")
636                 argName ~= ", -"~arg.altName~argSuffix;
637    
638             string nameColumnWidthStr = "%s".format(nameColumnWidth);
639             *argStr ~= format("%s%-"~nameColumnWidthStr~"s%s%s\n",
640                               indent, argName~" ", requiredStr~arg.desc, defaultValStr);
641         }
642         if(basicArgStr != "" && advancedArgStr != "")
643         {
644             basicArgStr = "\nBasic: \n"~basicArgStr;
645             advancedArgStr = "\nAdvanced: \n"~advancedArgStr;
646         }
647         return ret~basicArgStr~advancedArgStr;
648     }
649
650     string getDetailedUsage()
651     {
652         string ret;
653         string indent = "  ";
654         string basicArgStr;
655         string advancedArgStr;
656        
657         ret ~= "Switches: (prefixes can be '/', '-' or '--')\n";
658         foreach(Arg arg; args)
659         {
660             string* argStr = arg.isAdvanced? &advancedArgStr : &basicArgStr;
661
662             string argName = arg.isSwitchless? "" : "-"~arg.name;
663             if(arg.altName != "")
664                 argName ~= ", -"~arg.altName;
665             if(!arg.isSwitchless || arg.altName != "")
666                 argName ~= " ";
667    
668             // For some reason, unbox can't see Arg's private member "defaultValue"
669             auto argDefaultValue = arg.defaultValue;
670             mixin(unbox!(argDefaultValue, "val"));
671
672             string defaultVal;
673             string requiredStr;
674             string toLowerStr;
675             string switchlessStr;
676             string advancedStr;
677
678             if(valAsInt)
679                 defaultVal = "%s".format(valAsInt());
680             else if(valAsInts)
681                 defaultVal = "%s".format(valAsInts());
682             else if(valAsBool)
683                 defaultVal = "%s".format(valAsBool());
684             else if(valAsBools)
685                 defaultVal = "%s".format(valAsBools());
686             else if(valAsStr)
687                 defaultVal = `"%s"`.format(valAsStr());
688             else if(valAsStrs)  //TODO: Change this one from [ blah ] to [ "blah" ]
689                 defaultVal = "%s".format(valAsStrs());
690
691             defaultVal    = arg.isRequired   ? "" : ", Default: "~defaultVal;
692             requiredStr   = arg.isRequired   ? "Required" : "Optional";
693             toLowerStr    = arg.toLower      ? ", Case-Insensitive" : "";
694             switchlessStr = arg.isSwitchless ? ", Nameless" : "";
695             advancedStr   = arg.isAdvanced   ? ", Advanced" : ", Basic";
696            
697             *argStr ~= "\n";
698             *argStr ~= format("%s(%s), %s%s%s%s%s\n",
699                               argName, getArgTypeName(arg),
700                               requiredStr, switchlessStr, toLowerStr, advancedStr, defaultVal);
701             *argStr ~= format("%s\n", arg.desc);
702         }
703         ret ~= basicArgStr;
704         ret ~= advancedArgStr;
705         ret ~= "\n";
706         ret ~= switchTypesMsg;
707         return ret;
708     }
709 }
Note: See TracBrowser for help on using the browser.