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 }