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 }