| 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 |
} |
|---|