root/misc/tango/optparse.d

Revision 98, 29.4 kB (checked in by KirkMcDonald, 2 years ago)

Added default argument features to Tango version of optparse.

Line 
1 /*
2 Copyright (c) 2007 Kirk McDonald
3
4 Permission is hereby granted, free of charge, to any person obtaining a copy of
5 this software and associated documentation files (the "Software"), to deal in
6 the Software without restriction, including without limitation the rights to
7 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
8 of the Software, and to permit persons to whom the Software is furnished to do
9 so, subject to the following conditions:
10
11 The above copyright notice and this permission notice shall be included in all
12 copies or substantial portions of the Software.
13
14 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 SOFTWARE.
21 */
22 /**
23  * Command-line option parsing, in the style of Python's optparse.
24  *
25  * Refer to the complete docs for more information.
26  */
27 module optparse;
28
29 import tango.io.Stdout : Stdout;
30 import tango.text.Util : locate, locatePrior;
31 import tango.text.Ascii : toUpper;
32 import tango.stdc.stdlib : exit, EXIT_FAILURE, EXIT_SUCCESS;
33 import tango.text.convert.Integer : parse, toInt, toString = toUtf8;
34 import tango.text.convert.Utf : toUtf8, toUtf32;
35
36 /*
37 import std.stdio : writefln, writef;
38 import std.string : find, toupper;
39 import std.c.stdlib : exit, EXIT_FAILURE, EXIT_SUCCESS;
40 import std.conv : toInt, ConvError;
41 import std.path : getBaseName;
42 import std.utf : toUTF8, toUTF32;
43 */
44
45 /*
46 Options may be in two forms: long and short. Short options start with a single
47 dash and are one letter long. Long options start with two dashes and may
48 consist of any number of characters (so long as they don't start with a dash,
49 though they may contain dashes). Options are case-sensitive.
50
51 Short options may be combined. The following are equivalent:
52
53 $ myapp -a -b -c
54 $ myapp -abc
55 $ myapp -ab -c
56
57 If -f and --file are aliases of the same option, which accepts an argument,
58 the following are equivalent:
59
60 $ myapp -f somefile.txt
61 $ myapp -fsomefile.txt
62 $ myapp --file somefile.txt
63 $ myapp --file=somefile.txt
64
65 The following are also valid:
66
67 $ myapp -abcf somefile.txt
68 $ myapp -abcfsomefile.txt
69 $ myapp -abc --file somefile.txt
70
71 If an option occurs multiple times, the last one is the one recorded:
72
73 $ myapp -f somefile.txt --file otherfile.txt
74
75 Matches 'otherfile.txt'.
76 */
77
78 bool startswith(char[] s, char[] start) {
79     if (s.length < start.length) return false;
80     return s[0 .. start.length] == start;
81 }
82 bool endswith(char[] s, char[] end) {
83     if (s.length < end.length) return false;
84     return s[$ - end.length .. $] == end;
85 }
86
87 /// Thrown if client code tries to set up an improper option.
88 class OptionError : Exception {
89     this(char[] msg) { super(msg); }
90 }
91 // Thrown if client code tries to extract the wrong type from an option.
92 class OptionTypeError : Exception {
93     this(char[] msg) { super(msg); }
94 }
95
96 /++
97 This class represents the results after parsing the command-line.
98 +/
99 class Options {
100     char[][][char[]] opts;
101     int[char[]] counted_opts;
102     /// By default, leftover arguments are placed in this array.
103     char[][] args;
104
105     /// Retrieves the results of the Store and StoreConst actions.
106     char[] opIndex(char[] opt) {
107         char[][]* o = opt in opts;
108         if (o) {
109             return (*o)[0];
110         } else {
111             return "";
112         }
113     }
114     /// Retrieves the results of the Store action, when the type is Integer.
115     int value(char[] opt) {
116         char[][]* o = opt in opts;
117         if (o) {
118             return toInt((*o)[0]);
119         } else {
120             return 0;
121         }
122     }
123     /// Retrieves the results of the Append and AppendConst actions.
124     char[][] list(char[] opt) {
125         char[][]* o = opt in opts;
126         if (o) {
127             return *o;
128         } else {
129             return null;
130         }
131     }
132     /// Retrieves the results of the Append action, when the type is Integer.
133     int[] valueList(char[] opt) {
134         char[][]* o = opt in opts;
135         int[] l;
136         if (o) {
137             l.length = (*o).length;
138             foreach (i, s; *o) {
139                 l[i] = toInt(s);
140             }
141         }
142         return l;
143     }
144     /// Retrieves the results of the Count action.
145     int count(char[] opt) {
146         int* c = opt in counted_opts;
147         if (c) {
148             return *c;
149         } else {
150             return 0;
151         }
152     }
153     /// Retrieves the results of the SetTrue and SetFalse actions.
154     bool flag(char[] opt) {
155         char[][]* o = opt in opts;
156         if (o) {
157             return (*o)[0] == "1";
158         } else {
159             return false;
160         }
161     }
162 }
163
164 // Options, args, this opt's index in args, name[, arg]
165 ///
166 alias void delegate(Options, inout char[][], inout int, char[], char[]) OptionCallbackFancyArg;
167 ///
168 alias void delegate(Options, inout char[][], inout int, char[], int)    OptionCallbackFancyInt;
169 ///
170 alias void delegate(Options, inout char[][], inout int, char[])         OptionCallbackFancy;
171
172 ///
173 alias void delegate(char[]) OptionCallbackArg;
174 ///
175 alias void delegate(int)    OptionCallbackInt;
176 ///
177 alias void delegate()       OptionCallback;
178
179 /*
180 Actions:
181  * Store:        name
182  * StoreConst:   name, const_value
183  * Append:       name
184  * AppendConst:  name, const_value
185  * Count:        name
186  * CallbackArg:  dga
187  * CallbackVoid: dg
188 */
189 ///
190 enum Action { /+++/Store, /+++/StoreConst, /+++/Append, /+++/AppendConst, /+++/Count, /+++/SetTrue, /+++/SetFalse, /+++/Callback, /+++/CallbackFancy, /+++/Help /+++/}
191 ///
192 enum ArgType { /+++/None, /+++/String, /+++/Integer /+++/}
193
194 ArgType defaultType(Action action) {
195     switch (action) {
196         case Action.Store, Action.Append, Action.Callback, Action.CallbackFancy:
197             return ArgType.String;
198             break;
199         default:
200             return ArgType.None;
201             break;
202     }
203 }
204
205 /++
206 This class represents a single command-line option.
207 +/
208 class Option {
209     char[][] shortopts, longopts;
210     Action action;
211     ArgType type;
212     char[] name, argname;
213     char[] const_value;
214
215     char[] default_string;
216     int default_value;
217     bool default_flag, has_default;
218
219     OptionCallbackArg callback;
220     OptionCallbackInt int_callback;
221     OptionCallback void_callback;
222
223     OptionCallbackFancyArg fancy_callback;
224     OptionCallbackFancyInt fancy_int_callback;
225     OptionCallbackFancy fancy_void_callback;
226     char[] helptext;
227     this(
228         char[][] shorts, char[][] longs, ArgType type,
229         Action act, char[] name, char[] const_value,
230         OptionCallbackArg dga, OptionCallback dg,
231         OptionCallbackInt dgi,
232         OptionCallbackFancyArg fdga,
233         OptionCallbackFancyInt fdgi,
234         OptionCallbackFancy fdg
235     ) {
236         this.shortopts = shorts;
237         this.longopts = longs;
238         this.action = act;
239         this.type = type;
240         this.name = name;
241         this.argname = toUpper(name.dup);
242         this.default_string = "";
243         this.default_value = 0;
244         this.default_flag = false;
245
246         // Perform sanity checks.
247         assert (name !is null);
248         switch (act) {
249             case Action.Store, Action.Append:
250                 assert(type != ArgType.None);
251                 break;
252             case Action.StoreConst, Action.AppendConst:
253                 assert(type == ArgType.None);
254                 assert(const_value !is null);
255                 break;
256             case Action.Callback:
257                 //assert(type != ArgType.None);
258                 switch (type) {
259                     case ArgType.String:
260                         assert(dga !is null);
261                         break;
262                     case ArgType.Integer:
263                         assert(dgi !is null);
264                         break;
265                     case ArgType.None:
266                         assert(dg !is null);
267                         break;
268                 }
269                 break;
270             case Action.CallbackFancy:
271                 switch (type) {
272                     case ArgType.String:
273                         assert(fdga !is null);
274                         break;
275                     case ArgType.Integer:
276                         assert(fdgi !is null);
277                         break;
278                     case ArgType.None:
279                         assert(fdg !is null);
280                         break;
281                 }
282             default:
283                 break;
284         }
285         this.const_value = const_value;
286         this.callback = dga;
287         this.int_callback = dgi;
288         this.void_callback = dg;
289         this.fancy_callback = fdga;
290         this.fancy_int_callback = fdgi;
291         this.fancy_void_callback = fdg;
292     }
293     char[] toString() {
294         int optCount = this.shortopts.length + this.longopts.length;
295         char[] result;
296         bool printed_arg = false;
297         foreach(i, opt; this.shortopts ~ this.longopts) {
298             result ~= opt;
299             if (i < optCount-1) {
300                 result ~= ", ";
301             } else if (this.hasArg()) {
302                 result ~= "=" ~ toUpper(this.argname.dup);
303             }
304         }
305         return result;
306     }
307     //enum Action { Store, StoreConst, Append, AppendConst, Count, SetTrue, SetFalse, Callback, CallbackFancy, Help }
308     void issue_default(Options results) {
309         // Only set the default if the option doesn't already have a value.
310         char[][]* val = this.name in results.opts;
311         switch (this.action) {
312             case Action.Store, Action.Append:
313                 if (val !is null) return;
314                 if (this.type == ArgType.String) {
315                     results.opts[name] = [default_string];
316                 } else {
317                     results.opts[name] = [.toString(default_value)];
318                 }
319                 break;
320             case Action.StoreConst, Action.AppendConst:
321                 if (val !is null) return;
322                 results.opts[name] = [default_string];
323                 break;
324             case Action.SetTrue, Action.SetFalse:
325                 if (val !is null) return;
326                 if (default_flag) {
327                     results.opts[name] = ["1"];
328                 } else {
329                     results.opts[name] = ["0"];
330                 }
331                 break;
332             default:
333                 return;
334         }
335     }
336     // Does whatever this option is supposed to do.
337     void performAction(OptionParser parser, Options results, inout char[][] args, inout int idx, char[] arg) {
338         int i;
339         if (this.type == ArgType.Integer) {
340             // Verify that it's an int.
341             i = parser.toOptInt(arg);
342         }
343         switch (this.action) {
344             case Action.Store:
345                 results.opts[name] = [arg];
346                 break;
347             case Action.Append:
348                 results.opts[name] ~= arg;
349                 break;
350             case Action.StoreConst:
351                 assert(arg is null, "Got unexpected argument for '"~name~"' option.");
352                 results.opts[name] = [const_value];
353                 break;
354             case Action.AppendConst:
355                 assert(arg is null, "Got unexpected argument for '"~name~"' option.");
356                 results.opts[name] ~= const_value;
357                 break;
358             case Action.Count:
359                 assert(arg is null, "Got unexpected argument for '"~name~"' option.");
360                 ++results.counted_opts[name];
361                 break;
362             case Action.SetTrue:
363                 results.opts[name] = ["1"];
364                 break;
365             case Action.SetFalse:
366                 results.opts[name] = ["0"];
367                 break;
368             case Action.Callback:
369                 switch (type) {
370                     case ArgType.String:
371                         callback(arg);
372                         break;
373                     case ArgType.Integer:
374                         int_callback(i);
375                         break;
376                     case ArgType.None:
377                         void_callback();
378                         break;
379                 }
380                 break;
381             case Action.CallbackFancy:
382                 switch (type) {
383                     case ArgType.String:
384                         fancy_callback(results, args, idx, name, arg);
385                         break;
386                     case ArgType.Integer:
387                         fancy_int_callback(results, args, idx, name, i);
388                         break;
389                     case ArgType.None:
390                         fancy_void_callback(results, args, idx, name);
391                         break;
392                 }
393                 break;
394             case Action.Help:
395                 parser.helpText();
396                 exit(EXIT_SUCCESS);
397                 break;
398         }
399     }
400     /// Returns whether this option accepts an argument.
401     bool hasArg() {
402         return this.type != ArgType.None;
403     }
404     /// Sets the help text for this option.
405     Option help(char[] help) {
406         this.helptext = help;
407         return this;
408     }
409     /// Sets the name of this option's argument, if it has one.
410     Option argName(char[] argname) {
411         this.argname = argname;
412         return this;
413     }
414     Option def(char[] val) {
415         if (
416             (this.type != ArgType.String || (this.action != Action.Store && this.action != Action.Append)) &&
417             this.action != Action.StoreConst && this.action != Action.AppendConst
418         )
419             throw new OptionError("Cannot specify string default for non-string option '"~this.name~"'");
420         this.has_default = true;
421         this.default_string = val;
422         return this;
423     }
424     Option def(int val) {
425         if (this.type != ArgType.Integer || (this.action != Action.Store && this.action != Action.Append))
426             throw new OptionError("Cannot specify integer default for non-integer option '"~this.name~"'");
427         this.has_default = true;
428         this.default_value = val;
429         return this;
430     }
431     Option def(bool val) {
432         if (this.action != Action.SetTrue && this.action != Action.SetFalse)
433             throw new OptionError("Cannot specify boolean default for non-flag option '"~this.name~"'");
434         this.has_default = true;
435         this.default_flag = val;
436         return this;
437     }
438     // Returns true if the passed option string matches this option.
439     bool matches(char[] _arg) {
440         dchar[] arg = toUtf32(_arg);
441         if (
442             arg.length < 2 ||
443             arg.length == 2 && (arg[0] != '-' || arg[1] == '-') ||
444             arg.length > 2 && (arg[0 .. 2] != "--" || arg[2] == '-')
445         ) {
446             return false;
447         }
448         if (arg.length == 2) {
449             foreach (opt; shortopts) {
450                 if (_arg == opt) {
451                     return true;
452                 }
453             }
454         } else {
455             foreach (opt; longopts) {
456                 if (_arg == opt) {
457                     return true;
458                 }
459             }
460         }
461         return false;
462     }
463 }
464
465 /++
466 This class is used to define a set of options, and parse the command-line
467 arguments.
468 +/
469 class OptionParser {
470     OptionCallbackArg leftover_cb;
471     /// An array of all of the options known by this parser.
472     Option[] options;
473     char[] name, desc;
474     /// The description of the programs arguments, as used in the Help action.
475     char[] argdesc;
476     private void delegate(char[]) error_callback;
477
478     this(char[] desc="") {
479         this.name = "";
480         this.desc = desc;
481         this.argdesc = "[options] args...";
482     }
483
484     /// Sets a callback, to override the default error behavior.
485     void setErrorCallback(void delegate(char[]) dg) {
486         error_callback = dg;
487     }
488     void unknownOptError(char[] opt) {
489         error("Unknown argument '"~opt~"'");
490     }
491     void expectedArgError(char[] opt) {
492         error("'"~opt~"' option expects an argument.");
493     }
494     /// Displays an error message and terminates the program.
495     void error(char[] err) {
496         if (error_callback !is null) {
497             error_callback(err);
498         } else {
499             this.helpText();
500             Stdout.println(err);
501         }
502         exit(EXIT_FAILURE);
503     }
504     int toOptInt(char[] s) {
505         int i;
506         uint ate;
507         i = .parse(s, 10u, &ate);
508         if (ate != s.length)
509             error("Could not convert '"~s~"' to an integer.");
510         return i;
511     }
512
513     /// Displays useful "help" information about the program's options.
514     void helpText() {
515         int optWidth;
516         char[][] optStrs;
517         typedef char spacechar = ' ';
518         spacechar[] padding;
519         // Calculate the maximum width of the option lists.
520         foreach(i, opt; options) {
521             optStrs ~= opt.toString();
522             if (optStrs[i].length > optWidth) {
523                 optWidth = optStrs[i].length;
524             }
525         }
526         Stdout.formatln("Usage: {0} {1}", this.name, this.argdesc);
527         if (this.desc !is null && this.desc != "") Stdout.println(this.desc);
528         Stdout.println("\nOptions:");
529         foreach(i, opt; options) {
530             padding.length = optWidth - optStrs[i].length;
531             Stdout.formatln("  {0}{1} {2}", optStrs[i], cast(char[])padding, opt.helptext);
532         }
533     }
534    
535     // Checks the passed arg against all the options in the parser.
536     // Returns null if no match is found.
537     Option matches(char[] arg) {
538         foreach(o; options) {
539             if (o.matches(arg)) {
540                 return o;
541             }
542         }
543         return null;
544     }
545     char[] getBaseName(char[] path) {
546         version(Windows) {
547             char delimiter = '\\';
548         } else {
549             char delimiter = '/';
550         }
551         uint idx = locatePrior(path, delimiter);
552         if (idx == path.length) return path;
553         return path[idx+1 .. $];
554     }
555     char[] getProgramName(char[] path) {
556         version(Windows) {
557             // (Unicode note: ".exe" only contains 4 code units, so this slice
558             // should Just Work.) (Although it remains to be seen how robust
559             // this code actually is.)
560             //Stdout.println(path);
561             //assert(path[$-4 .. $] == ".exe");
562             //path = path[0 .. $-4];
563         }
564         return getBaseName(path);
565     }
566     /// Parses the passed command-line arguments and returns the results.
567     Options parse(char[][] args) {
568         this.name = getProgramName(args[0]);
569         args = args[1 .. $];
570         Options options = new Options;
571         /*
572         The issue is this:
573
574         $ myapp -abc
575
576         This might be three short opts, or one or two opts, the last of which
577         accepts an argument. In the three-opt case, we want to get:
578
579         $ myapp -a -b -c
580
581         In the one-opt case, we want:
582
583         $ myapp -a bc
584
585         In the two-opt case, we want:
586
587         $