1 /++
2 + Authors: Christian Koestlin
3 + Copyright: Copyright © 2018, Christian Köstlin
4 + License: MIT
5 +/
6 module commandline;
7
8 import asciitable;
9 import std.algorithm;
10 import std.experimental.logger;
11 import std.range;
12 import std..string;
13 import std.typecons;
14 public import matcher;
15
16 auto toKv(ref string[] args)
17 {
18 auto arg = args.front;
19 string[] res;
20 if (arg.startsWith("--"))
21 {
22 arg = arg[2 .. $];
23 res = arg.split("=");
24 args.popFront;
25 if (res.length == 1)
26 {
27 if (args.empty)
28 {
29 res ~= "true";
30 }
31 else
32 {
33 throw new Exception("argument missing");
34 }
35 }
36 return res;
37 }
38 else
39 {
40 if (arg.startsWith("-"))
41 {
42 auto k = arg[1 .. $];
43 args.popFront;
44 if (args.empty)
45 {
46 return [k, "true"];
47 }
48 auto v = args.front;
49 args.popFront;
50 return [k, v];
51 }
52 else
53 {
54 return null;
55 }
56 }
57 }
58
59 alias ParseResult = Tuple!(string[string], "parsed", string[], "rest");
60
61 bool isLongOption(string s)
62 {
63 return s.startsWith("--");
64 }
65
66 bool isShortOption(string s)
67 {
68 return s.startsWith("-");
69 }
70
71 /++
72 + parses args (takes out all options and returns the rest).
73 +/
74 ParseResult parse(Option[] options, string[] args)
75 {
76 string[string] keyValues;
77 foreach (option; options)
78 {
79 if (option.defaultValue != null)
80 {
81 keyValues[option.name] = option.defaultValue;
82 }
83 }
84 while (!args.empty)
85 {
86 auto arg = args.front;
87 if (arg.isLongOption)
88 {
89 arg = arg[2 .. $];
90 auto kv = arg.split("=");
91 auto f = options.find!(i => i.name == kv[0]);
92 if (f.empty)
93 {
94 throw new Exception("Illegal option '%s'".format(arg));
95 }
96 auto option = f.front;
97 if (kv.length == 2)
98 {
99 auto v = kv[1];
100 option.accept(v);
101 keyValues[kv[0]] = kv[1];
102 }
103 else if (kv.length == 1)
104 {
105 auto v = "true";
106 option.accept(v);
107 keyValues[kv[0]] = v;
108 }
109 args.popFront;
110 }
111 else if (arg.isShortOption)
112 {
113 arg = arg[1 .. $];
114 auto f = options.find!(i => i.shortName == arg);
115 if (f.empty)
116 {
117 throw new Exception("Illegal option '%s'".format(arg));
118 }
119 args.popFront;
120 auto option = f.front;
121 if (args.empty)
122 {
123 auto v = "true";
124 option.accept(v);
125 keyValues[option.name] = v;
126 }
127 else
128 {
129 auto v = args.front;
130 option.accept(v);
131 keyValues[option.name] = v;
132 args.popFront;
133 }
134 }
135 else
136 {
137 break;
138 }
139 }
140 return ParseResult(keyValues, args);
141 }
142
143 struct Option
144 {
145 string name;
146 string shortName;
147 string defaultValue;
148 string description;
149 Matcher!string matcher = new Everything!string;
150
151 static Option boolWithName(string name)
152 {
153 return withName(name).allow(One!string.of("true", "false")).withDefault("false");
154 }
155
156 static Option withName(string name)
157 {
158 return Option(name).withShortName(name[0 .. 1]);
159 }
160
161 Option withShortName(string shortName)
162 {
163 return Option(name, shortName, defaultValue, description, matcher);
164 }
165
166 Option withDefault(string defaultValue)
167 {
168 return Option(name, shortName, defaultValue, description, matcher);
169 }
170
171 Option withDescription(string description)
172 {
173 return Option(name, shortName, defaultValue, description, matcher);
174 }
175
176 Option allow(Matcher!string matcher)
177 {
178 return Option(name, shortName, defaultValue, description, matcher);
179 }
180
181 void accept(string v)
182 {
183 matcher.accept(this.name, v);
184 }
185 }
186
187 struct Command
188 {
189 string name;
190 bool delegate(Command) runDelegate;
191 Option[] options;
192 Command[] subCommands;
193 string[string] parsed;
194 string[] rest;
195 Command* subCommand;
196 bool helpNeeded()
197 {
198 return parsed["help"] == "true";
199 }
200
201 Command parse(string[] args)
202 {
203 "Parsing command %s".format(name).trace;
204 auto result = options.parse(args);
205 "Parsed %s".format(result).trace;
206 parsed = result.parsed;
207 rest = result.rest;
208 if (result.rest.length > 0)
209 {
210 auto h = subCommands.find!("a.name == b")(result.rest.front);
211 if (!h.empty)
212 {
213 subCommand = &h.front;
214 subCommand.parse(result.rest[1 .. $]);
215 }
216 }
217 else
218 {
219 if (!subCommands.empty)
220 {
221 subCommand = &subCommands.front;
222 subCommand.parse(result.rest);
223 }
224 }
225 return this;
226 }
227
228 string help()
229 {
230 auto table = AsciiTable(1, 1, 1, 1).add("long", "short", "description", "allowed values");
231 foreach (option; options)
232 {
233 table.add("--" ~ option.name, option.shortName ? "-" ~ option.shortName
234 : "", option.description, "Accept " ~ option.matcher.toString);
235 }
236 auto res = "Options:\n" ~ table.toString(" ", " ");
237 if (!subCommands.empty)
238 {
239 res ~= "\nSubcommands:\n " ~ subCommands.map!("a.name").join("\n ");
240 }
241 return res;
242 }
243
244 void run()
245 {
246 if (runDelegate(this))
247 {
248 if (subCommand != null)
249 {
250 subCommand.run;
251 }
252 else
253 {
254 if (rest.length > 0)
255 {
256 throw new Exception("Cannot understand %s".format(rest));
257 }
258 }
259
260 }
261 }
262 }