1 module clid.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; 7 8 import clid.attributes; 9 import clid.help; 10 import clid.util; 11 import clid.validate; 12 13 alias StringConsumer = void delegate(string); 14 15 /* 16 private struct ParseState 17 { 18 bool expectArgument = false; 19 StringConsumer argumentConsumer = null; 20 } 21 */ 22 23 private struct ParseState(C) 24 { 25 bool expectArgument = false; 26 StringConsumer argumentConsumer = null; 27 mixin RequireStruct!C requires; 28 } 29 30 private mixin template RequireStruct(C) 31 { 32 static foreach (member; getSymbolsByUDA!(C, Required)) 33 { 34 mixin("bool " ~ member.stringof ~ ";"); 35 } 36 } 37 38 /** 39 * Parses a range of arguments. 40 * Params: args = The arguments to parse. 41 */ 42 C parse(C)(string[] args) 43 { 44 validateStruct!C(); 45 C c; 46 ParseState!C state; 47 48 foreach (string arg; args) 49 { 50 if (state.expectArgument == true) 51 { 52 state.argumentConsumer(arg); 53 state.expectArgument = false; 54 } 55 else if (arg == "-h" || arg == "--help") 56 { 57 printHelp(c); 58 exit(0); 59 } 60 else 61 { 62 parseArgument(state, c, arg); 63 } 64 } 65 66 if (state.expectArgument) 67 { 68 stderr.writeln("Incomplete argument"); 69 exit(1); 70 } 71 checkRequires(state); 72 73 return c; 74 } 75 76 private void checkRequires(C)(ref ParseState!C state) 77 { 78 bool failed = false; 79 static foreach (member; __traits(allMembers, state.requires)) 80 { 81 if (!mixin("state.requires." ~ member)) 82 { 83 stderr.writeln("Missing required argument --" ~ getUDAs!(Value!(C, 84 member), Parameter)[0].longName); 85 failed = true; 86 } 87 } 88 if (failed) 89 exit(1); 90 } 91 92 private void parseArgument(C)(ref ParseState!C state, ref C c, string arg) 93 { 94 if (arg.length < 2) 95 fail("Malformed argument '" ~ arg ~ "'"); 96 else if (arg[0] == '-' && arg[1] != '-') 97 { 98 foreach (flag; arg[1 .. $ - 1]) 99 { 100 parseShortArgument(state, c, flag); 101 } 102 parseShortArgument(state, c, arg[$ - 1], true); 103 } 104 else if (arg.length >= 3 && arg[0] == '-' && arg[1] == '-') 105 { 106 parseLongArgument(state, c, arg[2 .. $]); 107 } 108 else 109 fail("Malformed argument '" ~ arg ~ "'"); 110 } 111 112 private void parseShortArgument(C)(ref ParseState!C state, ref C c, dchar flag, 113 immutable bool allowArgs = false) 114 { 115 foreach (member; __traits(allMembers, C)) 116 { 117 static if (hasUDA!(__traits(getMember, c, member), Parameter)) 118 { 119 if (getUDAs!(__traits(getMember, c, member), Parameter)[0].shortName == flag) 120 { 121 static if (is(typeof(__traits(getMember, c, member)) == bool)) 122 { 123 __traits(getMember, c, member) = true; 124 return; 125 } 126 else 127 { 128 if (allowArgs) 129 { 130 import std.conv : to; 131 132 state.expectArgument = true; 133 state.argumentConsumer = (value) { 134 static if (hasUDAV!(c, member, Required)) 135 mixin("state.requires." ~ member ~ " = true;"); 136 __traits(getMember, c, member) = to!(typeof(__traits(getMember, 137 c, member)))(value); 138 }; 139 return; 140 } 141 else 142 fail("Illegal argument " ~ flag.text); 143 } 144 } 145 } 146 } 147 fail("Unkown argument " ~ flag.text); 148 } 149 150 private void parseLongArgument(C)(ref ParseState!C state, ref C c, string arg) 151 { 152 foreach (member; __traits(allMembers, C)) 153 { 154 static if (hasUDA!(__traits(getMember, c, member), Parameter)) 155 { 156 if (getUDAs!(__traits(getMember, c, member), Parameter)[0].longName == arg) 157 { 158 static if (is(typeof(__traits(getMember, c, member)) == bool)) 159 { 160 __traits(getMember, c, member) = true; 161 return; 162 } 163 else 164 { 165 import std.conv : to; 166 167 state.expectArgument = true; 168 state.argumentConsumer = (value) { 169 static if (hasUDAV!(c, member, Required)) 170 mixin("state.requires." ~ member ~ " = true;"); 171 __traits(getMember, c, member) = to!(typeof(__traits(getMember, c, member)))( 172 value); 173 }; 174 return; 175 } 176 } 177 } 178 } 179 fail("Illegal argument " ~ arg); 180 } 181 182 private void fail(string message) 183 { 184 writeln(message); 185 exit(1); 186 } 187 188 /** 189 * Validates a configuration struct. 190 * Each property should be marked using @Validate!validator. 191 * The validator will be called using the argument used (e.g.: "--help"), 192 * and with the actual value given. 193 * Params: c = The configuration struct to validate. 194 */ 195 void validateConfig(C)(C c) // @suppress(dscanner.suspicious.unused_parameter) 196 { 197 foreach (member; __traits(allMembers, C)) 198 { 199 static if (hasUDA!(__traits(getMember, C, member), Parameter)) 200 { 201 Parameter parameter = getUDAs!(__traits(getMember, C, member), Parameter)[0]; 202 foreach (uda; getUDAs!(__traits(getMember, C, member), Validate)) 203 { 204 if (__traits(getMember, c, member) !is null) 205 { 206 if (uda("--" ~ parameter.longName, __traits(getMember, c, member)) == false) 207 exit(1); 208 } 209 } 210 } 211 } 212 } 213 214 // ====================== 215 // Unit tests start here. 216 // ====================== 217 218 unittest 219 { 220 struct Config 221 { 222 @Parameter("foo", 'f') 223 string value; 224 225 @Parameter("num", 'n') 226 int number; 227 228 @Parameter("req", 'r') 229 @Required int required; 230 231 @Parameter("bool", 'b') 232 bool b; 233 } 234 235 immutable Config config = parse!Config(["--foo", "a_string", "-b", "-n", "5", "--req", "1"]); 236 import std.stdio : writeln; 237 238 assert(config.value == "a_string", "String value not read from arguments"); 239 assert(config.number == 5, "Integer value not read from arguments"); 240 assert(config.b == true, "Bool not set from arguments"); 241 } 242 243 unittest 244 { 245 246 struct Config 247 { 248 @Parameter("bool", 'b') 249 bool b; 250 251 @Parameter("file", 'f') @Validate!doesNotExist string file; 252 } 253 254 immutable Config config = parse!Config(["-bf", "some-random-file-that-should-not-exist"]); 255 config.validateConfig(); 256 import std.stdio : writeln; 257 258 assert(config.file == "some-random-file-that-should-not-exist", 259 "String not read from arguments"); 260 assert(config.b == true, "Bool not set from arguments"); 261 }