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 }