1 /**
2 Provides some convenience functionality for parsing command-line arguments
3 by piggy-backing off std.getopt.
4 
5 Authors: Tony J. Hudgins
6 Copyright: Copyright © 2019, Tony J. Hudgins
7 License: MIT
8 */
9 module dext.args;
10 
11 import std.traits   : hasUDA, getUDAs, fullyQualifiedName;
12 import std.getopt   : GetOptException, Option;
13 import std.format   : format;
14 
15 private {
16     T ctor( T, U... )( U args ) if( is( T == class ) || is( T == struct ) )
17     {
18         static if( is( T == class ) )
19             return new T( args );
20         else
21             return T( args );
22     }
23 
24     template getSingleUDA( alias where, alias what, alias orElse )
25     {
26         static if( !hasUDA!( where, what ) )
27             enum getSingleUDA = orElse;
28         else
29         {
30             alias udas = getUDAs!( where, what );
31             static assert(
32                 udas.length == 1,
33                 "%s cannot have more than one %s attribute".format(
34                     __traits( identifier, where ),
35                     fullyQualifiedName!what
36                 )
37             );
38 
39             enum getSingleUDA = udas[0];
40         }
41     }
42 
43     enum help(   alias field ) = getSingleUDA!( field, Help,      cast(string)null );
44     enum banner( alias type  ) = getSingleUDA!( type,  Banner,    cast(string)null );
45 
46     template flag( alias field )
47     {
48         enum ident = __traits( identifier, field );
49 
50         static if( hasUDA!( field, ShortName ) )
51             enum flag = "%s|%s".format( cast(char)getSingleUDA!( field, ShortName, char.init ), ident );
52         else
53             enum flag = ident;
54     }
55 
56     string escape( string s )
57     {
58         import std..string : replace;
59         return `"%s"`.format( s.replace( "\"", "\\\"" ) );
60     }
61 
62     string defaultFormatter( string banner, Option[] options )
63     {
64         import std.array  : appender;
65         import std.getopt : defaultGetoptFormatter;
66 
67         auto formatted = appender!string;
68         defaultGetoptFormatter( formatted, banner, options );
69 
70         return formatted.data;
71     }
72 
73     mixin template SingletonUDA( T )
74     {
75         private T _value;
76         alias _value this;
77 
78         this() @disable;
79         this( T value ) { this._value = value; }
80     }
81 }
82 
83 /++
84 A single printable character representing a short flag for a command line argument.
85 e.x. '-v'
86 
87 Authors: Tony J. Hudgins
88 Copyright: Copyright © 2019, Tony J. Hudgins
89 License: MIT
90 +/
91 struct ShortName
92 {
93     mixin SingletonUDA!char;
94 
95     invariant
96     {
97         import std.uni : isControl;
98         if( this._value.isControl )
99             throw new GetOptException( "short name must be a printable character" );
100     }
101 }
102 
103 /++
104 Help string for an option that will be displayed on the usage screen.
105 
106 Authors: Tony J. Hudgins
107 Copyright: Copyright © 2019, Tony J. Hudgins
108 License: MIT
109 +/
110 struct Help   { mixin SingletonUDA!string; }
111 
112 /++
113 A banner that will be printed to the usage screen before the options.
114 
115 Authors: Tony J. Hudgins
116 Copyright: Copyright © 2019, Tony J. Hudgins
117 License: MIT
118 +/
119 struct Banner { mixin SingletonUDA!string; }
120 
121 /++
122 Indicates that an option is required.
123 
124 Authors: Tony J. Hudgins
125 Copyright: Copyright © 2019, Tony J. Hudgins
126 License: MIT
127 +/
128 struct Required             { }
129 
130 /++
131 Indicates that that argument parsing is case-sensitive. Incompatible with [CaseInsensitive].
132 
133 Authors: Tony J. Hudgins
134 Copyright: Copyright © 2019, Tony J. Hudgins
135 License: MIT
136 +/
137 struct CaseSensitive        { }
138 
139 /++
140 Indicates that argument parsing is case-insensitive. Incompatible with [CaseSensitive].
141 
142 Authors: Tony J. Hudgins
143 Copyright: Copyright © 2019, Tony J. Hudgins
144 License: MIT
145 +/
146 struct CaseInsensitive      { }
147 
148 /++
149 Allow short options to be bundled e.g. '-xvf'. Incompatible with [NoBundling].
150 
151 Authors: Tony J. Hudgins
152 Copyright: Copyright © 2019, Tony J. Hudgins
153 License: MIT
154 +/
155 struct AllowBundling        { }
156 
157 /++
158 Disallow short options to be bundled. Incompatible with [AllowBundling].
159 
160 See_Also: AllowBundling
161 Authors: Tony J. Hudgins
162 Copyright: Copyright © 2019, Tony J. Hudgins
163 License: MIT
164 +/
165 struct NoBundling           { }
166 
167 /++
168 Pass unrecognized options through silently. Incompatible with [NoPassThrough].
169 
170 Authors: Tony J. Hudgins
171 Copyright: Copyright © 2019, Tony J. Hudgins
172 License: MIT
173 +/
174 struct PassThrough          { }
175 
176 /++
177 Don't pass unrecognized options through silently. Incompatible with [PassThrough].
178 
179 Authors: Tony J. Hudgins
180 Copyright: Copyright © 2019, Tony J. Hudgins
181 License: MIT
182 +/
183 struct NoPassThrough        { }
184 
185 /++
186 Stops processing on the first string that doesn't look like an option.
187 
188 Authors: Tony J. Hudgins
189 Copyright: Copyright © 2019, Tony J. Hudgins
190 License: MIT
191 +/
192 struct StopOnFirstNonOption { }
193 
194 /++
195 Keep the end-of-options marker string.
196 
197 Authors: Tony J. Hudgins
198 Copyright: Copyright © 2019, Tony J. Hudgins
199 License: MIT
200 +/
201 struct KeepEndOfOptions     { }
202 
203 alias OptionFormatter = string delegate( string, Option[] );
204 
205 /++
206 Use std.getopt.getopt to parse command-line arguments into a class or struct.
207 
208 This function automatically invokes the constructor for the class or struct and requires a public, parameterless constructor to work.
209 Does not take opCall() into account for classes.
210 
211 The caller can optionally provide an [OptionFormatter] delegate to format options into a prett-printed string to display on the help screen.
212 If no delegate is provided the default std.getopt.defaultGetoptFormatter function will be used as a fallback.
213 
214 Automatically prints the help text and exits the process when help is requested.
215 
216 Params:
217     args = The array of arguments provided in main().
218     formatter = Optional option formatter.
219 
220 Throws: std.getopt.GetoptException if parsing fails.
221 
222 Examples:
223 --------------------
224 struct MyOptions
225 {
226     @ShortName( 'v' )
227     bool verbose;
228 }
229 
230 void main( string[] args )
231 {
232     import std.stdio;
233 
234     auto parsed = args.parseArgs!MyOptions;
235     writelfn( "is verbose? %s", parsed.verbose ? "yes" : "no" );
236 }
237 --------------------
238 
239 Authors: Tony J. Hudgins
240 Copyright: Copyright © 2019, Tony J. Hudgins
241 License: MIT
242 +/
243 T parseArgs( T )( ref string[] args, lazy OptionFormatter formatter = null )
244     if( is( T == class ) || is( T == struct ) )
245 {
246     auto x = ctor!T;
247     parseArgs( args, x, formatter );
248     return x;
249 }
250 
251 /++
252 Use std.getopt.getopt to parse command-line arguments into a class or struct.
253 
254 This function takes a reference to an already existing instance of the destination class or struct if manual construction
255 is needed prior to parsing.
256 
257 The caller can optionally provide an [OptionFormatter] delegate to format options into a prett-printed string to display on the help screen.
258 If no delegate is provided the default std.getopt.defaultGetoptFormatter function will be used as a fallback.
259 
260 Automatically prints the help text and exits the process when help is requested.
261 
262 Params:
263     args = The array of arguments provided in main().
264     instance = The instance that will contain parsed values.
265     formatter = Optional option formatter.
266 
267 Throws: std.getopt.GetoptException if parsing fails.
268 
269 Examples:
270 --------------------
271 struct MyOptions
272 {
273     @ShortName( 'v' )
274     bool verbose;
275 
276     string optional;
277 
278     this( string optional )
279     {
280         this.optional = optional;
281     }
282 }
283 
284 void main( string[] args )
285 {
286     import std.stdio;
287 
288     auto parsed = MyOptions( "some default value" );
289     args.parseArgs( parsed );
290     writelfn( "optional value: %s", parsed.optional );
291 }
292 --------------------
293 
294 Authors: Tony J. Hudgins
295 Copyright: Copyright © 2019, Tony J. Hudgins
296 License: MIT
297 +/
298 void parseArgs( T )( ref string[] args, ref T instance, lazy OptionFormatter formatter = null )
299     if( is( T == class ) || is( T == struct ) )
300 {
301     import std.getopt : config, getopt, defaultGetoptFormatter, Option;
302     import std..string : join;
303 
304     enum string[] getoptArgs = {
305         import std.traits : FieldNameTuple;
306         import std.conv   : to;
307 
308         string[] mixArgs;
309 
310         static if( hasUDA!( T, CaseSensitive ) && hasUDA!( T, CaseInsensitive ) )
311             static assert( false, "@CaseSensitive and @CaseInsensitive are mutually exclusive" );
312         
313         static if( hasUDA!( T, AllowBundling ) && hasUDA!( T, NoBundling ) )
314             static assert( false, "@AllowBundling and @NoBundling are mutually exclusive" );
315         
316         static if( hasUDA!( T, PassThrough ) && hasUDA!( T, NoPassThrough ) )
317             static assert( false, "@PassThrough and @NoPassThrough are mutually exclusive" );
318         
319         static if( hasUDA!( T, CaseSensitive ) )
320             mixArgs ~= "config.caseSensitive";
321         
322         static if( hasUDA!( T, CaseInsensitive ) )
323             mixArgs ~= "config.caseInsensitive";
324         
325         static if( hasUDA!( T, AllowBundling ) )
326             mixArgs ~= "config.allowBundling";
327         
328         static if( hasUDA!( T, NoBundling ) )
329             mixArgs ~= "config.noBundling";
330         
331         static if( hasUDA!( T, PassThrough ) )
332             mixArgs ~= "config.passThrough";
333         
334         static if( hasUDA!( T, NoPassThrough ) )
335             mixArgs ~= "config.noPassThrough";
336         
337         static if( hasUDA!( T, StopOnFirstNonOption ) )
338             mixArgs ~= "config.stopOnFirstNonOption";
339         
340         static if( hasUDA!( T, KeepEndOfOptions ) )
341             mixArgs ~= "config.keepEndOfOptions";
342         
343         static foreach( i, name; FieldNameTuple!T )
344         {
345             static if( hasUDA!( __traits( getMember, instance, name ), Required ) )
346                 mixArgs ~= "config.required";
347             
348             mixArgs ~= flag!( __traits( getMember, instance, name ) ).escape();
349             
350             static if( hasUDA!( __traits( getMember, instance, name ), Help ) )
351                 mixArgs ~= help!( __traits( getMember, instance, name ) ).escape();
352             
353             mixArgs ~= "&instance.%s".format( name );
354         }
355         
356         return mixArgs;
357     }();
358 
359     mixin( "auto result = getopt( args, %s );".format( getoptArgs.join( "," ) ) );
360     
361     if( result.helpWanted )
362     {
363         import std.functional   : toDelegate;
364         import std.stdio        : stderr;
365         import core.stdc.stdlib : exit;
366 
367         auto fn = formatter is null ? toDelegate( &defaultFormatter ) : formatter;
368         auto formatted = fn( cast(string) banner!T, result.options );
369         stderr.writeln( formatted );
370         stderr.flush();
371 
372         exit( -1 );
373     }
374 }
375 
376 version( unittest )
377 {
378     enum Color { red, green, blue }
379 
380     @Banner( "My Super Cool App" )
381     @CaseSensitive
382     struct MyOptions
383     {
384         @Help( "be noisy" )
385         @ShortName( 'v' )
386         @Required
387         bool verbose;
388 
389         @Required
390         @Help( "what color" )
391         @ShortName( 'c' )
392         Color color;
393     }
394 }
395 
396 @system unittest
397 {
398     auto args = [ "test.exe", "-v", "--color=green" ];
399     auto parsed = args.parseArgs!MyOptions;
400 
401     assert( parsed.verbose );
402     assert( parsed.color == Color.green );
403 }