1 /// The various datatypes provided by infogen. 2 module jaster.cli.infogen.datatypes; 3 4 import std.typecons : Flag, Nullable; 5 import jaster.cli.parser, jaster.cli.infogen, jaster.cli.result; 6 7 /// Used with `Pattern.matchSpacefull`. 8 alias AllowPartialMatch = Flag!"partialMatch"; 9 10 /++ 11 + Attach any value from this enum onto an argument to specify what parsing action should be performed on it. 12 + ++/ 13 enum CommandArgAction 14 { 15 /// Perform the default parsing action. 16 default_, 17 18 /++ 19 + Increments an argument for every time it is defined inside the parameters. 20 + 21 + Arg Type: Named 22 + Value Type: Any type that supports `++`. 23 + Arg becomes optional: true 24 + ++/ 25 count, 26 } 27 28 /++ 29 + Describes the existence of a command argument. i.e., how many times can it appear; is it optional, etc. 30 + ++/ 31 enum CommandArgExistence 32 { 33 /// Can only appear once, and is mandatory. 34 default_ = 0, 35 36 /// Argument can be omitted. 37 optional = 1 << 0, 38 39 /// Argument can be redefined. 40 multiple = 1 << 1, 41 } 42 43 /++ 44 + Describes the parsing scheme used when parsing the argument's value. 45 + ++/ 46 enum CommandArgParseScheme 47 { 48 /// Default parsing scheme. 49 default_, 50 51 /// Parsing scheme that special cases bools. 52 bool_, 53 54 /// Allows: -v, -vvvv(n+1). Special case: -vsome_value ignores the "some_value" and leaves it for the next parse cycle. 55 allowRepeatedName 56 } 57 58 /++ 59 + Describes a command and its parameters. 60 + 61 + Params: 62 + CommandT = The command that this information belongs to. 63 + 64 + See_Also: 65 + `jaster.cli.infogen.gen.getCommandInfoFor` for generating instances of this struct. 66 + ++/ 67 struct CommandInfo(CommandT) 68 { 69 /// The command's `Pattern`, if it has one. 70 Pattern pattern; 71 72 /// The command's description. 73 string description; 74 75 /// Information about all of this command's named arguments. 76 NamedArgumentInfo!CommandT[] namedArgs; 77 78 /// Information about all of this command's positional arguments. 79 PositionalArgumentInfo!CommandT[] positionalArgs; 80 81 /// Information about this command's raw list argument, if it has one. 82 Nullable!(RawListArgumentInfo!CommandT) rawListArg; 83 } 84 85 /// The function used to perform an argument's setter action. 86 alias ArgumentActionFunc(CommandT) = Result!void function(string value, ref CommandT commandInstance); 87 88 /++ 89 + Contains information about command's argument. 90 + 91 + Params: 92 + UDA = The UDA that defines the argument (e.g. `@CommandNamedArg`, `@CommandPositionalArg`) 93 + CommandT = The command type that this argument belongs to. 94 + 95 + See_Also: 96 + `jaster.cli.infogen.gen.getCommandInfoFor` for generating instances of this struct. 97 + ++/ 98 struct ArgumentInfo(UDA, CommandT) 99 { 100 // NOTE: Do not use Nullable in this struct as it causes compile-time errors. 101 // It hits a code path that uses memcpy, which of course doesn't work in CTFE. 102 103 /// The result of `__traits(identifier)` on the argument's symbol. 104 string identifier; 105 106 /// The UDA attached to the argument's symbol. 107 UDA uda; 108 109 /// The binding action performed to create the argument's value. 110 CommandArgAction action; 111 112 /// The user-defined `CommandArgGroup`, this is `.init` for the default group. 113 CommandArgGroup group; 114 115 /// Describes the existence properties for this argument. 116 CommandArgExistence existence; 117 118 /// Describes how this argument is to be parsed. 119 CommandArgParseScheme parseScheme; 120 121 // I wish I could defer this to another part of the library instead of here. 122 // However, any attempt I've made to keep around aliases to parameters has resulted 123 // in a dreaded "Cannot infer type from template arguments CommandInfo!CommandType". 124 // 125 // My best guesses are: 126 // 1. More weird behaviour with the hidden context pointer D inserts. 127 // 2. I might've hit some kind of internal template limit that the compiler is just giving a bad message for. 128 129 /// The function used to perform the binding action for this argument. 130 ArgumentActionFunc!CommandT actionFunc; 131 } 132 133 alias NamedArgumentInfo(CommandT) = ArgumentInfo!(CommandNamedArg, CommandT); 134 alias PositionalArgumentInfo(CommandT) = ArgumentInfo!(CommandPositionalArg, CommandT); 135 alias RawListArgumentInfo(CommandT) = ArgumentInfo!(CommandRawListArg, CommandT); 136 137 /++ 138 + A pattern is a simple string format for describing multiple "patterns" that can be matched to user provided input. 139 + 140 + Description: 141 + A simple pattern of "hello" would match, and only match "hello". 142 + 143 + A pattern of "hello|world" would match either "hello" or "world". 144 + 145 + Some patterns may contain spaces, other may not, it should be documented if possible. 146 + ++/ 147 struct Pattern 148 { 149 import std.algorithm : all; 150 import std.ascii : isWhite; 151 152 /// The raw pattern string. 153 string pattern; 154 155 //invariant(pattern.length > 0, "Attempting to use null pattern."); 156 157 /// Asserts that there is no whitespace within the pattern. 158 void assertNoWhitespace() const 159 { 160 assert(this.pattern.all!(c => !c.isWhite), "The pattern '"~this.pattern~"' is not allowed to contain whitespace."); 161 } 162 163 /// Returns: An input range consisting of every subpattern within this pattern. 164 auto byEach() 165 { 166 import std.algorithm : splitter; 167 return this.pattern.splitter('|'); 168 } 169 170 /++ 171 + The default subpattern can be used as the default 'user-facing' name to display to the user. 172 + 173 + Returns: 174 + Either the first subpattern, or "DEFAULT" if this pattern is null. 175 + ++/ 176 string defaultPattern() 177 { 178 return (this.pattern is null) ? "DEFAULT" : this.byEach.front; 179 } 180 181 /++ 182 + Matches the given input string without splitting up by spaces. 183 + 184 + Params: 185 + toTestAgainst = The string to test for. 186 + 187 + Returns: 188 + `true` if there was a match for the given string, `false` otherwise. 189 + ++/ 190 bool matchSpaceless(string toTestAgainst) 191 { 192 import std.algorithm : any; 193 return this.byEach.any!(str => str == toTestAgainst); 194 } 195 /// 196 unittest 197 { 198 assert(Pattern("v|verbose").matchSpaceless("v")); 199 assert(Pattern("v|verbose").matchSpaceless("verbose")); 200 assert(!Pattern("v|verbose").matchSpaceless("lalafell")); 201 } 202 203 /++ 204 + Advances the given token parser in an attempt to match with any of this pattern's subpatterns. 205 + 206 + Description: 207 + On successful or partial match (if `allowPartial` is `yes`) the given `parser` will be advanced to the first 208 + token that is not part of the match. 209 + 210 + e.g. For the pattern ("hey there"), if you matched it with the tokens ["hey", "there", "me"], the resulting parser 211 + would only have ["me"] left. 212 + 213 + On a failed match, the given parser is left unmodified. 214 + 215 + Bugs: 216 + If a partial match is allowed, and a partial match is found before a valid full match is found, then only the 217 + partial match is returned. 218 + 219 + Params: 220 + parser = The parser to match against. 221 + allowPartial = If `yes` then allow partial matches, otherwise only allow full matches. 222 + 223 + Returns: 224 + `true` if there was a full or partial (if allowed) match, otherwise `false`. 225 + ++/ 226 bool matchSpacefull(ref ArgPullParser parser, AllowPartialMatch allowPartial = AllowPartialMatch.no) 227 { 228 import std.algorithm : splitter; 229 230 foreach(subpattern; this.byEach) 231 { 232 auto savedParser = parser.save(); 233 bool isAMatch = true; 234 bool isAPartialMatch = false; 235 foreach(split; subpattern.splitter(" ")) 236 { 237 if(savedParser.empty 238 || !(savedParser.front.type == ArgTokenType.Text && savedParser.front.value == split)) 239 { 240 isAMatch = false; 241 break; 242 } 243 244 isAPartialMatch = true; 245 savedParser.popFront(); 246 } 247 248 if(isAMatch || (isAPartialMatch && allowPartial)) 249 { 250 parser = savedParser; 251 return true; 252 } 253 } 254 255 return false; 256 } 257 /// 258 unittest 259 { 260 // Test empty parsers. 261 auto parser = ArgPullParser([]); 262 assert(!Pattern("v").matchSpacefull(parser)); 263 264 // Test that the parser's position is moved forward correctly. 265 parser = ArgPullParser(["v", "verbose"]); 266 assert(Pattern("v").matchSpacefull(parser)); 267 assert(Pattern("verbose").matchSpacefull(parser)); 268 assert(parser.empty); 269 270 // Test that a parser that fails to match isn't moved forward at all. 271 parser = ArgPullParser(["v", "verbose"]); 272 assert(!Pattern("lel").matchSpacefull(parser)); 273 assert(parser.front.value == "v"); 274 275 // Test that a pattern with spaces works. 276 parser = ArgPullParser(["give", "me", "chocolate"]); 277 assert(Pattern("give me").matchSpacefull(parser)); 278 assert(parser.front.value == "chocolate"); 279 280 // Test that multiple patterns work. 281 parser = ArgPullParser(["v", "verbose"]); 282 assert(Pattern("lel|v|verbose").matchSpacefull(parser)); 283 assert(Pattern("lel|v|verbose").matchSpacefull(parser)); 284 assert(parser.empty); 285 } 286 }