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 validateConfig(c); 73 74 return c; 75 } 76 77 private void checkRequires(C)(ref ParseState!C state) 78 { 79 bool failed = false; 80 static foreach (member; __traits(allMembers, state.requires)) 81 { 82 if (!mixin("state.requires." ~ member)) 83 { 84 stderr.writeln("Missing required argument --" ~ getUDAs!(Value!(C, 85 member), Parameter)[0].longName); 86 failed = true; 87 } 88 } 89 if (failed) 90 exit(1); 91 } 92 93 private void parseArgument(C)(ref ParseState!C state, ref C c, string arg) 94 { 95 if (arg.length < 2) 96 fail("Malformed argument '" ~ arg ~ "'"); 97 else if (arg[0] == '-' && arg[1] != '-') 98 { 99 foreach (flag; arg[1 .. $ - 1]) 100 { 101 parseShortArgument(state, c, flag); 102 } 103 parseShortArgument(state, c, arg[$ - 1], true); 104 } 105 else if (arg.length >= 3 && arg[0] == '-' && arg[1] == '-') 106 { 107 parseLongArgument(state, c, arg[2 .. $]); 108 } 109 else 110 fail("Malformed argument '" ~ arg ~ "'"); 111 } 112 113 private void parseShortArgument(C)(ref ParseState!C state, ref C c, dchar flag, 114 immutable bool allowArgs = false) 115 { 116 foreach (member; __traits(allMembers, C)) 117 { 118 static if (hasUDA!(__traits(getMember, c, member), Parameter)) 119 { 120 if (getUDAs!(__traits(getMember, c, member), Parameter)[0].shortName == flag) 121 { 122 static if (is(typeof(__traits(getMember, c, member)) == bool)) 123 { 124 __traits(getMember, c, member) = true; 125 return; 126 } 127 else 128 { 129 if (allowArgs) 130 { 131 import std.conv : to; 132 133 state.expectArgument = true; 134 state.argumentConsumer = (value) { 135 static if (hasUDAV!(c, member, Required)) 136 mixin("state.requires." ~ member ~ " = true;"); 137 __traits(getMember, c, member) = to!(typeof(__traits(getMember, 138 c, member)))(value); 139 }; 140 return; 141 } 142 else 143 fail("Illegal argument " ~ flag.text); 144 } 145 } 146 } 147 } 148 fail("Unkown argument " ~ flag.text); 149 } 150 151 private void parseLongArgument(C)(ref ParseState!C state, ref C c, string arg) 152 { 153 foreach (member; __traits(allMembers, C)) 154 { 155 static if (hasUDA!(__traits(getMember, c, member), Parameter)) 156 { 157 if (getUDAs!(__traits(getMember, c, member), Parameter)[0].longName == arg) 158 { 159 static if (is(typeof(__traits(getMember, c, member)) == bool)) 160 { 161 __traits(getMember, c, member) = true; 162 return; 163 } 164 else 165 { 166 import std.conv : to; 167 168 state.expectArgument = true; 169 state.argumentConsumer = (value) { 170 static if (hasUDAV!(c, member, Required)) 171 mixin("state.requires." ~ member ~ " = true;"); 172 __traits(getMember, c, member) = to!(typeof(__traits(getMember, c, member)))( 173 value); 174 }; 175 return; 176 } 177 } 178 } 179 } 180 fail("Illegal argument " ~ arg); 181 } 182 183 private void fail(string message) 184 { 185 writeln(message); 186 exit(1); 187 } 188 189 /** 190 * Validates a configuration struct. 191 * Each property should be marked using @Validate!validator. 192 * The validator will be called using the argument used (e.g.: "--help"), 193 * and with the actual value given. 194 * Params: c = The configuration struct to validate. 195 */ 196 void validateConfig(C)(ref C c) // @suppress(dscanner.suspicious.unused_parameter) 197 { 198 foreach (member; __traits(allMembers, C)) 199 { 200 static if (hasUDA!(__traits(getMember, C, member), Parameter)) 201 { 202 Parameter parameter = getUDAs!(__traits(getMember, C, member), Parameter)[0]; 203 foreach (uda; getUDAs!(__traits(getMember, C, member), Validate)) 204 { 205 static if (is(typeof(__traits(getMember, c, member)) == string)) 206 { 207 if (__traits(getMember, c, member) !is null) 208 { 209 if (uda("--" ~ parameter.longName, __traits(getMember, c, member)) == false) 210 exit(1); 211 } 212 } 213 else 214 { 215 if (uda("--" ~ parameter.longName, __traits(getMember, c, member)) == false) 216 exit(1); 217 } 218 } 219 } 220 } 221 } 222 223 // ====================== 224 // Unit tests start here. 225 // ====================== 226 227 unittest 228 { 229 struct Config 230 { 231 @Parameter("foo", 'f') 232 string value; 233 234 @Parameter("num", 'n') 235 int number; 236 237 @Parameter("req", 'r') 238 @Required int required; 239 240 @Parameter("bool", 'b') 241 bool b; 242 } 243 244 immutable Config config = parse!Config([ 245 "--foo", "a_string", "-b", "-n", "5", "--req", "1" 246 ]); 247 import std.stdio : writeln; 248 249 assert(config.value == "a_string", "String value not read from arguments"); 250 assert(config.number == 5, "Integer value not read from arguments"); 251 assert(config.b == true, "Bool not set from arguments"); 252 } 253 254 unittest 255 { 256 257 struct Config 258 { 259 @Parameter("bool", 'b') 260 bool b; 261 262 @Parameter("file", 'f') @Validate!doesNotExist string file; 263 } 264 265 immutable Config config = parse!Config([ 266 "-bf", "some-random-file-that-should-not-exist" 267 ]); 268 config.validateConfig(); 269 import std.stdio : writeln; 270 271 assert(config.file == "some-random-file-that-should-not-exist", 272 "String not read from arguments"); 273 assert(config.b == true, "Bool not set from arguments"); 274 }