1 module argparse.parser; 2 3 import argparse; 4 import argparse.internal; 5 6 /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 7 8 package struct Parser 9 { 10 import std.sumtype: SumType; 11 12 struct Unknown {} 13 struct EndOfArgs {} 14 struct Positional {} 15 struct NamedShort { 16 string name; 17 string nameWithDash; 18 string value = null; // null when there is no value 19 } 20 struct NamedLong { 21 string name; 22 string nameWithDash; 23 string value = null; // null when there is no value 24 } 25 26 alias Argument = SumType!(Unknown, EndOfArgs, Positional, NamedShort, NamedLong); 27 28 immutable Config config; 29 30 string[] args; 31 string[] unrecognizedArgs; 32 33 bool[size_t] idxParsedArgs; 34 size_t idxNextPositional = 0; 35 36 struct CmdParser 37 { 38 Result delegate(const ref Argument) parse; 39 Result delegate(const ref Argument) complete; 40 const(string)[] completeSuggestion; 41 bool isDefault; 42 } 43 CmdParser[] cmdStack; 44 45 Argument splitArgumentNameValue(string arg) 46 { 47 import std.string : indexOf; 48 49 if(arg.length == 0) 50 return Argument.init; 51 52 if(arg == config.endOfArgs) 53 return Argument(EndOfArgs.init); 54 55 if(arg[0] != config.namedArgChar) 56 return Argument(Positional.init); 57 58 if(arg.length == 1 || arg.length == 2 && arg[1] == config.namedArgChar) 59 return Argument.init; 60 61 auto idxAssignChar = config.assignChar == char.init ? -1 : arg.indexOf(config.assignChar); 62 63 immutable string nameWithDash = idxAssignChar < 0 ? arg : arg[0 .. idxAssignChar]; 64 immutable string value = idxAssignChar < 0 ? null : arg[idxAssignChar + 1 .. $]; 65 66 return arg[1] == config.namedArgChar 67 ? Argument(NamedLong (nameWithDash[2..$], nameWithDash, value)) 68 : Argument(NamedShort(nameWithDash[1..$], nameWithDash, value)); 69 } 70 71 auto parseArgument(T, PARSE)(PARSE parse, ref T receiver, string value, string nameWithDash, size_t argIndex) 72 { 73 auto res = parse(config, nameWithDash, receiver, value, args); 74 if(!res) 75 return res; 76 77 idxParsedArgs[argIndex] = true; 78 79 return Result.Success; 80 } 81 82 auto parseSubCommand(T)(const ref CommandArguments!T cmd, ref T receiver) 83 { 84 import std.range: front, popFront; 85 86 auto found = cmd.findSubCommand(args.front); 87 if(found.parse is null) 88 return Result.UnknownArgument; 89 90 if(found.level < cmdStack.length) 91 cmdStack.length = found.level; 92 93 cmdStack ~= CmdParser((const ref arg) => found.parse(config, this, arg, false, receiver), (const ref arg) => found.complete(config, this, arg, false, receiver)); 94 95 found.initialize(receiver); 96 args.popFront(); 97 98 return Result.Success; 99 } 100 101 auto parse(bool completionMode, T)(const ref CommandArguments!T cmd, bool isDefaultCmd, ref T receiver, Unknown) 102 { 103 static if(completionMode) 104 { 105 import std.range: front, popFront; 106 import std.algorithm: filter; 107 import std.string:startsWith; 108 import std.array:array; 109 if(args.length == 1) 110 { 111 // last arg means we need to provide args and subcommands 112 auto A = args[0] == "" ? cmd.completeSuggestion : cmd.completeSuggestion.filter!(_ => _.startsWith(args[0])).array; 113 return Result(0, Result.Status.success, "", A); 114 } 115 } 116 117 return Result.UnknownArgument; 118 } 119 120 auto parse(bool completionMode, T)(const ref CommandArguments!T cmd, bool isDefaultCmd, ref T receiver, EndOfArgs) 121 { 122 static if(!completionMode) 123 { 124 import std.range: popFront; 125 126 args.popFront(); 127 128 cmd.setTrailingArgs(receiver, args); 129 unrecognizedArgs ~= args; 130 } 131 132 args = []; 133 134 return Result.Success; 135 } 136 137 auto parse(bool completionMode, T)(const ref CommandArguments!T cmd, bool isDefaultCmd, ref T receiver, Positional) 138 { 139 auto foundArg = cmd.findPositionalArgument(idxNextPositional); 140 if(foundArg.arg is null) 141 return parseSubCommand(cmd, receiver); 142 143 auto res = parseArgument(cmd.getParseFunction!completionMode(foundArg.index), receiver, null, foundArg.arg.names[0], foundArg.index); 144 if(!res) 145 return res; 146 147 idxNextPositional++; 148 149 return Result.Success; 150 } 151 152 auto parse(bool completionMode, T)(const ref CommandArguments!T cmd, bool isDefaultCmd, ref T receiver, NamedLong arg) 153 { 154 import std.algorithm : startsWith; 155 import std.range: popFront; 156 157 auto foundArg = cmd.findNamedArgument(arg.name); 158 159 if(foundArg.arg is null && arg.name.startsWith("no-")) 160 { 161 foundArg = cmd.findNamedArgument(arg.name[3..$]); 162 if(foundArg.arg is null || !foundArg.arg.allowBooleanNegation) 163 return Result.UnknownArgument; 164 165 arg.value = "false"; 166 } 167 168 if(foundArg.arg is null) 169 return Result.UnknownArgument; 170 171 if(isDefaultCmd && foundArg.arg.ignoreInDefaultCommand) 172 return Result.UnknownArgument; 173 174 args.popFront(); 175 return parseArgument(cmd.getParseFunction!completionMode(foundArg.index), receiver, arg.value, arg.nameWithDash, foundArg.index); 176 } 177 178 auto parse(bool completionMode, T)(const ref CommandArguments!T cmd, bool isDefaultCmd, ref T receiver, NamedShort arg) 179 { 180 import std.range: popFront; 181 182 auto foundArg = cmd.findNamedArgument(arg.name); 183 if(foundArg.arg !is null) 184 { 185 if(isDefaultCmd && foundArg.arg.ignoreInDefaultCommand) 186 return Result.UnknownArgument; 187 188 args.popFront(); 189 return parseArgument(cmd.getParseFunction!completionMode(foundArg.index), receiver, arg.value, arg.nameWithDash, foundArg.index); 190 } 191 192 // Try to parse "-ABC..." where "A","B","C" are different single-letter arguments 193 do 194 { 195 auto name = [arg.name[0]]; 196 foundArg = cmd.findNamedArgument(name); 197 if(foundArg.arg is null) 198 return Result.UnknownArgument; 199 200 // In case of bundling there can be no or one argument value 201 if(config.bundling && foundArg.arg.minValuesCount.get > 1) 202 return Result.UnknownArgument; 203 204 // In case of NO bundling there MUST be one argument value 205 if(!config.bundling && foundArg.arg.minValuesCount.get != 1) 206 return Result.UnknownArgument; 207 208 string value; 209 if(foundArg.arg.minValuesCount == 0) 210 arg.name = arg.name[1..$]; 211 else 212 { 213 // Bundling case: try to parse "-ABvalue" where "A","B" are different single-letter arguments and "value" is a value for "B" 214 // No bundling case: try to parse "-Avalue" where "A" is a single-letter argument and "value" is its value 215 value = arg.name[1..$]; 216 arg.name = ""; 217 } 218 219 auto res = parseArgument(cmd.getParseFunction!completionMode(foundArg.index), receiver, value, "-"~name, foundArg.index); 220 if(!res) 221 return res; 222 } 223 while(arg.name.length > 0); 224 225 args.popFront(); 226 return Result.Success; 227 } 228 229 auto parse(bool completionMode, T)(const ref CommandArguments!T cmd, bool isDefaultCmd, ref T receiver, Argument arg) 230 { 231 import std.sumtype: match; 232 233 return arg.match!(_ => parse!completionMode(cmd, isDefaultCmd, receiver, _)); 234 } 235 236 auto parse(bool completionMode)(Argument arg) 237 { 238 import std.range: front, popFront; 239 240 auto result = Result.Success; 241 242 const argsCount = args.length; 243 244 foreach_reverse(cmdParser; cmdStack) 245 { 246 static if(completionMode) 247 { 248 auto res = cmdParser.complete(arg); 249 if(res) 250 result.suggestions ~= res.suggestions; 251 } 252 else 253 { 254 auto res = cmdParser.parse(arg); 255 256 if(res.status != Result.Status.unknownArgument) 257 return res; 258 } 259 } 260 261 if(args.length > 0 && argsCount == args.length) 262 { 263 unrecognizedArgs ~= args.front; 264 args.popFront(); 265 } 266 267 return result; 268 } 269 270 auto parseAll(bool completionMode, T)(const ref CommandArguments!T cmd, ref T receiver) 271 { 272 import std.range: empty, front; 273 274 cmdStack ~= CmdParser( 275 (const ref arg) 276 { 277 return parse!completionMode(cmd, false, receiver, arg); 278 }, 279 (const ref arg) 280 { 281 return parse!completionMode(cmd, false, receiver, arg); 282 }, 283 cmd.completeSuggestion); 284 285 auto found = cmd.findSubCommand(DEFAULT_COMMAND); 286 if(found.parse !is null) 287 { 288 auto p = CmdParser((const ref arg) => found.parse(config, this, arg, true, receiver)); 289 p.isDefault = true; 290 cmdStack ~= p; 291 found.initialize(receiver); 292 } 293 294 while(!args.empty) 295 { 296 static if(completionMode) 297 auto res = parse!completionMode(args.length > 1 ? splitArgumentNameValue(args.front) : Argument.init); 298 else 299 auto res = parse!completionMode(splitArgumentNameValue(args.front)); 300 if(!res) 301 return res; 302 303 static if(completionMode) 304 if(args.empty) 305 return res; 306 } 307 308 return cmd.checkRestrictions(idxParsedArgs, config); 309 } 310 } 311 312 unittest 313 { 314 assert(Parser.init.splitArgumentNameValue("") == Parser.Argument(Parser.Unknown.init)); 315 assert(Parser.init.splitArgumentNameValue("-") == Parser.Argument(Parser.Unknown.init)); 316 assert(Parser.init.splitArgumentNameValue("--") == Parser.Argument(Parser.EndOfArgs.init)); 317 assert(Parser.init.splitArgumentNameValue("abc=4") == Parser.Argument(Parser.Positional.init)); 318 assert(Parser.init.splitArgumentNameValue("-abc") == Parser.Argument(Parser.NamedShort("abc", "-abc", null))); 319 assert(Parser.init.splitArgumentNameValue("--abc") == Parser.Argument(Parser.NamedLong("abc", "--abc", null))); 320 assert(Parser.init.splitArgumentNameValue("-abc=fd") == Parser.Argument(Parser.NamedShort("abc", "-abc", "fd"))); 321 assert(Parser.init.splitArgumentNameValue("--abc=fd") == Parser.Argument(Parser.NamedLong("abc", "--abc", "fd"))); 322 assert(Parser.init.splitArgumentNameValue("-abc=") == Parser.Argument(Parser.NamedShort("abc", "-abc", ""))); 323 assert(Parser.init.splitArgumentNameValue("--abc=") == Parser.Argument(Parser.NamedLong("abc", "--abc", ""))); 324 assert(Parser.init.splitArgumentNameValue("-=abc") == Parser.Argument(Parser.NamedShort("", "-", "abc"))); 325 assert(Parser.init.splitArgumentNameValue("--=abc") == Parser.Argument(Parser.NamedLong("", "--", "abc"))); 326 } 327 328 /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 329 330 package static Result callParser(Config config, bool completionMode, COMMAND)(ref COMMAND receiver, string[] args, out string[] unrecognizedArgs) 331 { 332 auto parser = Parser(config, args); 333 334 auto command = CommandArguments!COMMAND(config); 335 auto res = parser.parseAll!completionMode(command, receiver); 336 337 static if(!completionMode) 338 { 339 if(res) 340 unrecognizedArgs = parser.unrecognizedArgs; 341 else if(res.errorMsg.length > 0) 342 config.onError(res.errorMsg); 343 } 344 345 return res; 346 } 347 348 ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////