1 module jcli.commandparser.parser; 2 3 import jcli.argbinder, jcli.argparser, jcli.core, jcli.introspect, std; 4 5 struct CommandParser(alias CommandT_, alias ArgBinderInstance_ = ArgBinder!()) 6 { 7 alias CommandT = CommandT_; 8 alias CommandInfo = commandInfoFor!CommandT; 9 alias ArgBinderInstance = ArgBinderInstance_; 10 11 static ResultOf!CommandT parse()(string[] args) 12 { 13 return parse(ArgParser(args)); 14 } 15 16 static ResultOf!CommandT parse()(ArgParser parser) 17 { 18 CommandT command; 19 20 enum MaxPositionals = CommandInfo.positionalArgs.length; 21 size_t positionCount; 22 Pattern[string] requiredNamed; // key is identifier 23 bool[string] namedFound; // key is identifier, value is dummy. 24 25 static foreach(arg; CommandInfo.namedArgs) 26 { 27 static if(!(arg.existence & ArgExistence.optional)) 28 requiredNamed[arg.identifier] = arg.uda.pattern; 29 } 30 31 while(!parser.empty) 32 { 33 auto arg = parser.front; 34 scope(exit) parser.popFront(); 35 36 if(arg.fullSlice == "--") 37 { 38 static if(CommandInfo.rawArg != typeof(CommandInfo.rawArg).init) 39 { 40 parser.popFront(); 41 getArg!(CommandInfo.rawArg)(command) = parser; 42 } 43 break; 44 } 45 46 OuterSwitch: final switch(arg.kind) with(ArgParser.Result.Kind) 47 { 48 case rawText: 49 { 50 Switch: switch(positionCount) 51 { 52 static foreach(i, positional; CommandInfo.positionalArgs) 53 { 54 case i: 55 auto result = ArgBinderInstance.bind!positional(arg.fullSlice, command); 56 if(!result.isOk) 57 return fail!CommandT(result.error); 58 break Switch; 59 } 60 61 // I might be hitting a compiler bug, because without this, the "positionCount++" is sometimes, 62 // not all the time, but sometimes unreachable 63 case 200302104: 64 break; 65 66 default: 67 static if(CommandInfo.overflowArg == typeof(CommandInfo.overflowArg).init) 68 return fail!CommandT("Too many positional arguments near '%s'. Expected %s positional arguments.".format(arg.fullSlice, MaxPositionals)); 69 else 70 { 71 getArg!(CommandInfo.overflowArg)(command) ~= arg.fullSlice; 72 break; 73 } 74 } 75 } 76 positionCount++; 77 break; 78 79 case argument: 80 static foreach(named; CommandInfo.namedArgs) 81 { 82 if(named.uda.pattern.match(arg.nameSlice, (named.config & ArgConfig.caseInsensitive) > 0).matched 83 || (named.scheme == ArgParseScheme.repeatableName && named.uda.pattern.patterns.any!(p => p.length == 1 && arg.nameSlice.all!(c => c == p[0])))) 84 { 85 static if(!(named.existence & ArgExistence.optional)) 86 requiredNamed[named.identifier] = named.uda.pattern; 87 if((named.existence & ArgExistence.multiple) == 0) 88 { 89 enforce((named.identifier in namedFound) is null, 90 "Named argument %s cannot be specified multiple times.".format(named.identifier) 91 ); 92 } 93 namedFound[named.identifier] = true; 94 95 static if(named.scheme == ArgParseScheme.bool_) 96 { 97 static assert(named.action == ArgAction.normal, "ArgParseScheme.bool_ conflicts with anything that isn't ArgAction.normal."); 98 99 auto value = true; 100 auto copy = parser; 101 copy.popFront(); 102 if(!copy.empty && copy.front.kind == rawText) 103 { 104 if(copy.front.fullSlice == "true" || copy.front.fullSlice == "false") 105 { 106 parser.popFront(); // Keep main parser in sync 107 value = copy.front.fullSlice.to!bool; 108 } 109 } 110 getArg!named(command) = value; 111 } 112 else static if(named.scheme == ArgParseScheme.normal) 113 { 114 static if(named.action == ArgAction.normal) 115 { 116 parser.popFront(); 117 if(parser.empty) 118 return fail!CommandT("Expected value after argument "~arg.fullSlice~" but hit end of args."); 119 if(parser.front.kind == argument) 120 return fail!CommandT("Expected value after argument "~arg.fullSlice~" but instead got argument "~parser.front.fullSlice); 121 122 auto result = ArgBinderInstance.bind!named(parser.front.fullSlice, command); 123 if(!result.isOk) 124 return fail!CommandT(result.error); 125 } 126 else static if(named.action == ArgAction.count) 127 getArg!named(command)++; 128 else static assert(false, "Update me please."); 129 } 130 else static if(named.scheme == ArgParseScheme.repeatableName) 131 { 132 static assert(named.action == ArgAction.count, "ArgParseScheme.bool_ conflicts with anything that isn't ArgAction.count."); 133 getArg!named(command) += arg.nameSlice.length; 134 } 135 break OuterSwitch; 136 } 137 } 138 return fail!CommandT("Unknown argument: "~arg.fullSlice); 139 } 140 } 141 142 enforce( 143 positionCount >= CommandInfo.positionalArgs.length, 144 "Expected %s positional arguments but got %s instead. Missing the following required positional arguments:%s".format( 145 CommandInfo.positionalArgs.length, positionCount, 146 CommandInfo.positionalArgs[positionCount..$] 147 .map!(arg => arg.uda.name.length ? arg.uda.name : "NO_NAME") 148 .fold!((a,b) => a~" "~b)("") 149 ) 150 ); 151 152 Pattern[] notFound; 153 foreach(k, v; requiredNamed) 154 { 155 if(!namedFound.byKey.any!(key => key == k)) 156 notFound ~= v; 157 } 158 159 if(notFound.length) 160 { 161 return fail!CommandT( 162 "The following required named arguments were not found: " 163 ~notFound.fold!((a,b) => a.length ? a~", "~b.pattern : b.pattern)("") 164 ); 165 } 166 167 return ok(command); 168 } 169 } 170 171 unittest 172 { 173 @Command("ab") 174 static struct S 175 { 176 @ArgPositional 177 string s; 178 179 180 @ArgNamed("abc") 181 string a; 182 183 @ArgNamed("b") 184 @(ArgExistence.multiple) 185 string b; 186 187 @ArgNamed("c") 188 bool c; 189 190 @ArgNamed("d") 191 bool d; 192 193 @ArgNamed("e") 194 bool e; 195 196 @ArgPositional 197 string f; 198 199 @ArgNamed("v") 200 @(ArgAction.count) 201 int v; 202 203 @ArgOverflow 204 string[] overflow; 205 206 @ArgRaw 207 ArgParser raw; 208 } 209 210 alias parser = CommandParser!S; 211 auto result = parser.parse([ 212 "abc", 213 "--abc=1", 214 "-b", "2", 215 "-b=3", 216 "-c", 217 "-d false", 218 "-e arg2", 219 "-vv", 220 "-vvvv", 221 "overflow1", 222 "overflow2", 223 "--", 224 "raw 1", 225 "raw 2", 226 ]); 227 assert(result.isOk, result.error); 228 auto value = result.value; 229 auto withoutRaw = value; 230 withoutRaw.raw = ArgParser.init; 231 assert(withoutRaw == S( 232 "abc", 233 "1", 234 "3", 235 true, 236 false, 237 true, 238 "arg2", 239 6, 240 ["overflow1", "overflow2"], 241 ), value.to!string); 242 assert(value.raw.equal(ArgParser(["raw 1", "raw 2"])), value.raw.to!string); 243 }