1 module clid.core.parse; 2 3 import std.stdio : writeln, stderr; 4 import core.stdc.stdlib : exit; 5 import std.traits : hasUDA, getUDAs, hasMember, getSymbolsByUDA; 6 import std.conv : text, to; 7 import std.typecons : Nullable; 8 import std.meta : Alias, AliasSeq; 9 10 import clid.core.help; 11 import clid.core.util; 12 import clid.attributes; 13 import clid.validate; 14 15 alias StringConsumer = void delegate(string); 16 17 private struct ParseState(C) 18 { 19 bool expectArgument = false; 20 StringConsumer argumentConsumer = null; 21 int namelessIndex = 0; 22 mixin RequireStruct!C requires; 23 } 24 25 private mixin template RequireStruct(C) 26 { 27 static foreach (member; getSymbolsByUDA!(C, Required)) 28 { 29 mixin("bool " ~ member.stringof ~ ";"); 30 } 31 } 32 33 /** 34 * Parses a range of arguments. 35 * Params: args = The arguments to parse. 36 */ 37 Nullable!C parse(C)(string[] args) 38 { 39 validateStruct!C(); 40 C c; 41 ParseState!C state; 42 43 foreach (string arg; args) 44 { 45 if (state.expectArgument == true) 46 { 47 state.argumentConsumer(arg); 48 state.expectArgument = false; 49 } 50 else if (arg == "-h" || arg == "--help") 51 { 52 printHelp(c); 53 return Nullable!C(); 54 } 55 else 56 { 57 parseArgument(state, c, arg); 58 } 59 } 60 61 if (state.expectArgument) 62 { 63 stderr.writeln("Incomplete argument"); 64 return Nullable!C(); 65 } 66 checkRequires(state); 67 validateConfig(c); 68 69 return Nullable!C(c); 70 } 71 72 private bool checkRequires(C)(ref ParseState!C state) 73 { 74 bool failed = false; 75 static foreach (member; __traits(allMembers, state.requires)) 76 { 77 if (!mixin("state.requires." ~ member)) 78 { 79 stderr.writeln("Missing required argument --" ~ getUDAs!(value!(C, 80 member), Parameter)[0].longName); 81 failed = true; 82 } 83 } 84 return !failed; 85 } 86 87 private bool parseArgument(C)(ref ParseState!C state, ref C c, string arg) 88 in(arg.length > 0, "Cannot parse empty argument") 89 { 90 if (arg[0] != '-') 91 return parseUnnamedArgument(state, c, arg); 92 else if (arg.length < 2) 93 return fail("Malformed argument '" ~ arg ~ "'"); 94 else if (arg[0] == '-' && arg[1] != '-') 95 { 96 foreach (flag; arg[1 .. $ - 1]) 97 { 98 if (!parseShortArgument(state, c, flag)) 99 return false; 100 } 101 return parseShortArgument(state, c, arg[$ - 1], true); 102 } 103 else if (arg.length >= 3 && arg[0] == '-' && arg[1] == '-') 104 { 105 return parseLongArgument(state, c, arg[2 .. $]); 106 } 107 else 108 return fail("Malformed argument '" ~ arg ~ "'"); 109 } 110 111 private bool parseShortArgument(C)(ref ParseState!C state, ref C c, dchar flag, 112 immutable bool allowArgs = false) 113 { 114 foreach (member; __traits(allMembers, C)) 115 { 116 static if (hasUDA!(__traits(getMember, c, member), Parameter)) 117 { 118 if (getUDAs!(__traits(getMember, c, member), Parameter)[0].shortName == flag) 119 { 120 static if (is(typeof(__traits(getMember, c, member)) == bool)) 121 { 122 __traits(getMember, c, member) = true; 123 return true; 124 } 125 else 126 { 127 if (allowArgs) 128 { 129 import std.conv : to; 130 131 state.expectArgument = true; 132 state.argumentConsumer = (value) { 133 static if (hasUDAV!(c, member, Required)) 134 mixin("state.requires." ~ member ~ " = true;"); 135 __traits(getMember, c, member) = to!(typeof(__traits(getMember, 136 c, member)))(value); 137 }; 138 return true; 139 } 140 else 141 return fail("Illegal argument " ~ flag.text); 142 } 143 } 144 } 145 } 146 return fail("Unkown argument " ~ flag.text); 147 } 148 149 private bool parseLongArgument(C)(ref ParseState!C state, ref C c, string arg) 150 { 151 foreach (member; __traits(allMembers, C)) 152 { 153 static if (hasUDA!(__traits(getMember, c, member), Parameter)) 154 { 155 if (getUDAs!(__traits(getMember, c, member), Parameter)[0].longName == arg) 156 { 157 static if (is(typeof(__traits(getMember, c, member)) == bool)) 158 { 159 __traits(getMember, c, member) = true; 160 return true; 161 } 162 else 163 { 164 state.expectArgument = true; 165 state.argumentConsumer = (value) { 166 static if (hasUDAV!(c, member, Required)) 167 mixin("state.requires." ~ member ~ " = true;"); 168 __traits(getMember, c, member) = to!(typeof(__traits(getMember, c, member)))( 169 value); 170 }; 171 return true; 172 } 173 } 174 } 175 } 176 return fail("Illegal argument " ~ arg); 177 } 178 179 private bool parseUnnamedArgument(C)(ref ParseState!C state, ref C c, string arg) 180 { 181 auto setters = getSetters!C; 182 183 setters[state.namelessIndex++](c, arg); 184 return false; 185 } 186 187 private auto getSetters(C)() 188 { 189 void function(ref C, string)[getUnnamedMembers!(C).length] setters; 190 static foreach (i, member; getUnnamedMembers!(C)) 191 { 192 setters[i] = function(ref C c, string arg) { 193 mixin("c." ~ member ~ " = arg;"); 194 }; 195 } 196 return setters; 197 } 198 199 private template getUnnamedMembers(C) 200 { 201 alias getUnnamedMembers = getUnnamedMembersIn!(C, __traits(allMembers, C)); 202 } 203 204 private template getUnnamedMembersIn(C, members...) 205 { 206 static if (isUnnamedParameter!(C, members[0])) 207 { 208 static if (members.length == 1) 209 alias getUnnamedMembersIn = AliasSeq!(members[0]); 210 else 211 alias getUnnamedMembersIn = AliasSeq!(members[0], 212 getUnnamedMembersIn!(C, members[1 .. $])); 213 } 214 else static if (members.length == 1) 215 alias getUnnamedMembersIn = AliasSeq!(); 216 else 217 alias getUnnamedMembersIn = getUnnamedMembersIn!(C, members[1 .. $]); 218 } 219 220 private bool fail(string message) 221 { 222 stderr.writeln(message); 223 return false; 224 } 225 226 /** 227 * Validates a configuration struct. 228 * Each property should be marked using @Validate!validator. 229 * The validator will be called using the argument used (e.g.: "--help"), 230 * and with the actual value given. 231 * Params: c = The configuration struct to validate. 232 */ 233 bool validateConfig(C)(ref C c) // @suppress(dscanner.suspicious.unused_parameter) 234 { 235 foreach (member; __traits(allMembers, C)) 236 { 237 static if (hasUDA!(__traits(getMember, C, member), Parameter)) 238 { 239 Parameter parameter = getUDAs!(__traits(getMember, C, member), Parameter)[0]; 240 foreach (uda; getUDAs!(__traits(getMember, C, member), Validate)) 241 { 242 static if (is(typeof(__traits(getMember, c, member)) == string)) 243 { 244 if (__traits(getMember, c, member) !is null) 245 { 246 if (uda("--" ~ parameter.longName, __traits(getMember, c, member)) == false) 247 return false; 248 } 249 } 250 else 251 { 252 if (uda("--" ~ parameter.longName, __traits(getMember, c, member)) == false) 253 return false; 254 } 255 } 256 } 257 } 258 return true; 259 } 260 261 // ====================== 262 // Unit tests start here. 263 // ====================== 264 265 unittest 266 { 267 struct Config 268 { 269 @Parameter("foo", 'f') 270 string value; 271 272 @Parameter("num", 'n') 273 int number; 274 275 @Parameter("req", 'r') 276 @Required int required; 277 278 @Parameter("bool", 'b') 279 bool b; 280 } 281 282 immutable Config config = parse!Config([ 283 "--foo", "a_string", "-b", "-n", "5", "--req", "1" 284 ]); 285 286 assert(config.value == "a_string", "String value not read from arguments"); 287 assert(config.number == 5, "Integer value not read from arguments"); 288 assert(config.b == true, "Bool not set from arguments"); 289 } 290 291 unittest 292 { 293 294 struct Config 295 { 296 @Parameter("bool", 'b') 297 bool b; 298 299 @Parameter("file", 'f') @Validate!doesNotExist string file; 300 } 301 302 immutable Config config = parse!Config([ 303 "-bf", "some-random-file-that-should-not-exist" 304 ]); 305 config.validateConfig(); 306 307 assert(config.file == "some-random-file-that-should-not-exist", 308 "String not read from arguments"); 309 assert(config.b == true, "Bool not set from arguments"); 310 } 311 312 unittest 313 { 314 struct Config 315 { 316 @Parameter() string file; 317 } 318 319 immutable config = parse!Config(["arg"]); 320 321 assert(config.file == "arg"); 322 } 323 324 unittest 325 { 326 struct Config 327 { 328 @Parameter() string unnamed; 329 } 330 331 assert(isUnnamedParameter!(Config, "unnamed") == true, "Parameter is unnamed"); 332 } 333 334 unittest 335 { 336 struct Config 337 { 338 @Parameter("named") string named; 339 @Parameter() string unnamed; 340 } 341 342 assert(getUnnamedMembers!(Config) == AliasSeq!("unnamed"), "Did not find unnamed parameters"); 343 }