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 }