1 module eph.args.parse; 2 3 import eph.args.arg: Argument; 4 import eph.args.param: Parameter; 5 import eph.args.err; 6 import std.string: indexOf; 7 8 import std.stdio; 9 10 11 private const ubyte DASH = 45; 12 private const ubyte EQUALS = 61; 13 14 private enum State { 15 // In between cli inputs (not sure what is next) 16 SEEK, 17 18 // A single "-" character has been seen, not yet sure if long or short flag 19 FLAG_START, 20 21 // Found "--" followed by a non-space character 22 IN_LONG, 23 24 // Found non-dash starting character 25 IN_PARAM, 26 27 // Found single dash followed by non-dash, non-space character 28 IN_SHORT, 29 30 // Found a short flag seeking a possible flag value 31 SEEK_OPT_PARAM, 32 33 // Found a short flag seeking a mandatory flag value 34 SEEK_REQ_PARAM, 35 36 // "-- " has been encountered, stop parsing flags 37 DONE 38 } 39 40 /** 41 * Argument Parser 42 */ 43 public class ArgParser { 44 private Argument[char] byShort; 45 private Argument[string] byLong; 46 private Parameter[] params; 47 48 private ubyte paramIndex; 49 private string[] unknown; 50 private State state; 51 private string[] after; 52 53 private Argument lastArg; 54 55 private ulong strPos; 56 57 this() {} 58 59 /** 60 * Register arguments with this parser instance. 61 * Returns the current ArgParser instance. 62 */ 63 public ArgParser register(Argument[] opts...) { 64 for(int i; i < opts.length; i++) { 65 const char c = opts[i].shortFlag(); 66 if(c != 0) { 67 byShort[c] = opts[i]; 68 } 69 70 const string l = opts[i].longFlag(); 71 if (l !is null && l.length > 0) { 72 byLong[l] = opts[i]; 73 } 74 } 75 76 return this; 77 } 78 79 /** 80 * Register parameters with this parser instance. 81 * Returns the current ArgParser instance. 82 */ 83 public ArgParser register(Parameter[] params...) { 84 this.params ~= params; 85 return this; 86 } 87 88 /** 89 * Parse the command input string. 90 */ 91 public void parse(const string[] args) { 92 debug(dOpts) printDebug(__FUNCTION__, args); 93 for(int i = 1; i < args.length; i++) { 94 strPos = 0uL; 95 if (state == State.DONE) { 96 after ~= args[i]; 97 } else { 98 parse(args[i]); 99 } 100 } 101 } 102 103 /** 104 * Parse state handler method 105 */ 106 private void parse(const string arg) { 107 debug(dOpts) printDebug(__FUNCTION__, arg); 108 while(strPos < arg.length) { 109 switch(this.state) { 110 case State.SEEK: 111 this.seek(arg[strPos]); 112 break; 113 114 case State.FLAG_START: 115 this.flagStart(arg[strPos], arg); 116 break; 117 118 case State.IN_PARAM: 119 this.inParam(arg); 120 break; 121 122 case State.IN_LONG: 123 this.inLong(arg); 124 break; 125 126 case State.IN_SHORT: 127 this.inShort(arg[strPos], arg); 128 break; 129 130 case State.SEEK_OPT_PARAM: 131 this.seekOptParam(arg[strPos], arg); 132 break; 133 134 case State.SEEK_REQ_PARAM: 135 this.seekReqParam(arg[strPos], arg); 136 break; 137 138 default: 139 throw new Exception("illegal parser state"); 140 } 141 } 142 } 143 144 /** 145 * Returns all parameters encountered in the command call 146 * that were not recognized by the parser. 147 */ 148 public string[] remainder() { 149 return unknown; 150 } 151 152 /** 153 * Returns all values passed to the command call after the 154 * appearance of "--" unchanged. 155 */ 156 public string[] passthrough() { 157 return after; 158 } 159 160 /** 161 * Seek state handler method 162 */ 163 private void seek(const char c) { 164 debug(dOpts) printDebug(__FUNCTION__, "" ~ c); 165 switch(c) { 166 167 case DASH: 168 this.state = State.FLAG_START; 169 strPos++; 170 break; 171 172 default: 173 this.state = State.IN_PARAM; 174 175 } 176 } 177 178 /** 179 * Flag start state handler method 180 */ 181 private void flagStart(const char c, const string raw) { 182 debug(dOpts) printDebug(__FUNCTION__, "" ~ c, raw); 183 if(isDash(c)) { 184 strPos++; 185 if(strPos == raw.length) { 186 state = State.DONE; 187 } else { 188 state = State.IN_LONG; 189 } 190 return; 191 } 192 193 state = State.IN_SHORT; 194 } 195 196 /** 197 * In param state handler method 198 */ 199 private void inParam(const string raw) { 200 debug(dOpts) printDebug(__FUNCTION__, raw); 201 202 if (paramIndex >= params.length) { 203 unknown ~= raw; 204 } else { 205 params[paramIndex].value(raw); 206 params[paramIndex].setWasSet(true); 207 paramIndex++; 208 } 209 210 strPos = raw.length; 211 state = State.SEEK; 212 } 213 214 /** 215 * In short flag state handler method 216 */ 217 private void inShort(const char c, const string raw) { 218 debug(dOpts) printDebug(__FUNCTION__, ""~c, raw); 219 220 if(!exists(c)) { 221 throw new UnknownFlagException(c); 222 } 223 224 Argument o = byShort[c]; 225 226 o.use(); 227 strPos++; 228 229 // If the arg does not take parameters, return now without changing the 230 // state so that this method will be called again to parse the next arg. 231 // use case: "foo -abc" where a, b, and c are separate args. 232 if(!o.parameterized()) { 233 234 // If we have reached the end of this string block then return to seek 235 // mode. 236 if(strPos == raw.length) 237 state = State.SEEK; 238 239 return; 240 } 241 242 // If the position has been moved past the end of the input string then 243 // there is nothing left to process and this is a singular flag expecting 244 // one or more spaces followed by the param value: "foo -f someVal". 245 if(strPos == raw.length) { 246 lastArg = o; 247 state = o.paramRequired() 248 ? State.SEEK_REQ_PARAM 249 : State.SEEK_OPT_PARAM; 250 return; 251 } 252 253 // If this is reached, then the arg is expecting a param, and there is more 254 // to the string, which either means user error or the arg param value was 255 // provided without a space: foo -Fvalue 256 o.value(raw[strPos..$]); 257 strPos = raw.length; 258 state = State.SEEK; 259 } 260 261 private void inLong(const string raw) { 262 debug(dOpts) printDebug(__FUNCTION__, raw); 263 const int ind = cast(int) raw.indexOf(EQUALS); 264 const bool hasValue = ind > -1; 265 266 string name; 267 string value; 268 269 strPos = raw.length; 270 state = State.SEEK; 271 272 if (!hasValue) { 273 name = raw[2..$]; 274 } else { 275 name = raw[2..ind]; 276 value = raw[ind+1..$]; 277 } 278 279 if (!exists(name)) { 280 throw new UnknownFlagException(name); 281 } 282 283 Argument arg = byLong[name]; 284 285 arg.use(); 286 if (hasValue) { 287 if (arg.parameterized()) { 288 arg.value(value); 289 return; 290 } 291 throw new UnexpectedParameterException(arg, value); 292 } 293 294 // Nothing left to do. 295 if(arg.hasOptionalParam() || !arg.parameterized()) { 296 return; 297 } 298 299 // Arg has mandatory param and no value. 300 throw new MissingParameterException(arg); 301 } 302 303 private void seekOptParam(const char c, const string raw) { 304 debug(dOpts) printDebug(__FUNCTION__, ""~c, raw); 305 if (isDash(c)) { 306 strPos++; 307 state = State.FLAG_START; 308 return; 309 } 310 311 strPos = raw.length; 312 state = State.SEEK; 313 lastArg.value(raw); 314 } 315 316 private void seekReqParam(const char c, const string raw) { 317 debug(dOpts) printDebug(__FUNCTION__, ""~c, raw); 318 if (isDash(c)) { 319 throw new MissingParameterException(lastArg); 320 } 321 322 strPos = raw.length; 323 state = state.SEEK; 324 lastArg.value(raw); 325 } 326 327 private bool exists(string key) const { 328 auto t = (key in byLong); 329 return t != null; 330 } 331 332 private bool exists(char key) const { 333 auto t = (key in byShort); 334 return t != null; 335 } 336 337 private bool isDash(const char c) const { 338 debug(dOpts) printDebug(__FUNCTION__, ""~c); 339 return c == DASH; 340 } 341 342 debug(dOpts) { 343 void printDebug(const string method, const string[] args...) const { 344 const string tmp = method[__MODULE__.length + 11..$]; 345 writefln("Pos: %2d | State: %-14s | Method: %s(%s)", strPos, state, tmp, args); 346 } 347 } 348 }