1 /** 2 Handles program commands, which are arguments passed to a program that are not prefixed with `-` or `--`. So that 3 you handle programs that can be executed in this format: 4 5 --- 6 $ ./program -v --global-option=7 command1 --parameter="yo" sub-command --another-parameter=42 7 --- 8 9 Usage: 10 11 The basic idea is to create a `ProgramCommands` structure and then define a number of `Command`s that it can handle and 12 optionally, a set of $(DDOX_NAMED_REF dcli.program_options, `ProgramOptions`) that it can handle. Each `Command` can in 13 turn have it's own `ProgramCommands`. 14 15 Handlers: 16 17 Handlers are provided to be able to eacily handle when a command is encountered. This will allow you to structure your 18 program with a module based architecture, where each "handler" can e.g. pass the commands to a module that is build 19 specifically for handling that command. 20 21 E.g.: 22 --- 23 // install.d 24 void commandHandler(T)(T commands) { 25 writeln(commands.install); // prints true 26 } 27 // main.d 28 static import install, build; 29 void main(string[] args) { 30 auto commands = ProgramCommands!( 31 Command!"build".handler!(build.commandHandler) 32 Command!"install".handler!(install.commandHandler) 33 )(); 34 commands.parse(args); 35 commands.executeHandlers(); 36 } 37 // Run it 38 $ ./program install 39 $ >> true 40 --- 41 42 Inner_ProgramOptions: 43 44 The first argument to a `ProgramCommands` object can optionally be a $(DDOX_NAMED_REF dcli.program_options, `ProgramOptions`) 45 object. In this case, the program options are accessible with an internal variable named `options` 46 47 E.g.: 48 --- 49 auto commands = ProgramCommands!( 50 ProgramOptions!(Options!("opt1", string)), 51 Command!"command1", 52 Command!"command2".args!( 53 ProgramOptions!(Options!("opt2", string)) 54 ) 55 )(); 56 57 commands.options.opt1; // access the option 58 commands.command1; // access the command 59 commands.command2.op2; // access the options of command2 60 --- 61 62 Inner_ProgramCommands: 63 64 You can also pass in sub commands to a `Command` object in the `args` parameter: 65 66 --- 67 auto commands = ProgramCommands!( 68 ProgramOptions!(Options!("opt1", string)), 69 Command!"command1".args!( 70 ProgramCommands!( 71 ProgramOptions!(Options!("opt2", string)), 72 Command!"sub-command1", 73 ), 74 ), 75 )(); 76 77 commands.options.opt1; // access the option 78 commands.command1.subCommand1; // access the command 79 commands.command1.options.op2; // access the options of command2 80 --- 81 */ 82 module dcli.program_commands; 83 84 /// 85 unittest { 86 alias MainCommands = ProgramCommands!( 87 ProgramOptions!( 88 Option!("glob1", string).shortName!"a".description!"desc", 89 ), 90 Command!"cmd1" 91 .args!( 92 ProgramOptions!( 93 Option!("opt1", string).shortName!"b".description!"desc", 94 ), 95 ), 96 Command!"cmd2" 97 .handler!(Fixtures.handleCommand2) 98 .description!"desc", 99 Command!"cmd3" 100 .args!( 101 ProgramCommands!( 102 ProgramOptions!( 103 Option!("opt3", string).shortName!"d".description!"desc", 104 ), 105 Command!"sub1" 106 .args!( 107 ProgramOptions!( 108 Option!("opt4", string).shortName!"e".description!"desc", 109 ), 110 ) 111 .handler!(Fixtures.handleCommand3) 112 .description!"desc", 113 ), 114 ) 115 .handler!(Fixtures.handleCommand3Sub1) 116 .description!"desc", 117 ); 118 119 auto commands = MainCommands(); 120 121 commands.parse([ 122 "-ayo", 123 "cmd3", 124 "-d", 125 "hi", 126 "sub1", 127 "-e", 128 "boo", 129 ]); 130 131 assert(cast(bool)commands.cmd1 == false); 132 assert(cast(bool)commands.cmd2 == false); 133 assert(cast(bool)commands.cmd3 == true); 134 135 assert(commands.options.glob1 == "yo"); 136 assert(commands.cmd3.options.opt3 == "hi"); 137 assert(commands.cmd3.sub1.opt4 == "boo"); 138 139 assert(commands.helpText == 140 `Options: 141 -a --glob1 desc 142 Commands: 143 cmd1 144 cmd2 desc 145 cmd3 desc` 146 ); 147 148 commands.executeHandlers; 149 150 assert(!Fixtures.checkResetHandledCommand2); 151 assert( Fixtures.checkResetHandledCommand3); 152 assert( Fixtures.checkResetHandledCommand3Sub1); 153 } 154 155 import dcli.program_options; 156 import ddash.lang: Void, isVoid; 157 import optional; 158 import ddash.algorithm: indexWhere; 159 import ddash.range: frontOr; 160 161 import std.algorithm: map; 162 import std.typecons; 163 import std.stdio: writeln; 164 165 // version = dcli_programcommands_debug; 166 167 private void debug_print(Args...)(Args args, int line = __LINE__, string file = __FILE__) @trusted @nogc { 168 version(dcli_programcommands_debug) { 169 debug { 170 import std.stdio: writeln; 171 import std.path: baseName, stripExtension, dirName, pathSplitter; 172 import std.range: array; 173 auto stripped = baseName(stripExtension(file)); 174 if (stripped == "package") { 175 stripped = pathSplitter(dirName(file)).array[$ - 1] ~ "/" ~ stripped; 176 } 177 writeln("[", stripped, ":", line, "] : ", args); 178 } 179 } 180 } 181 182 private template isCommand(T) { 183 import std.traits: isInstanceOf; 184 enum isCommand = isInstanceOf!(CommandImpl, T); 185 } 186 187 private template isProgramCommands(T) { 188 import std.traits: isInstanceOf; 189 enum isProgramCommands = isInstanceOf!(ProgramCommands, T); 190 } 191 192 private string makeValidCamelCase(string name)() if (name.length) { 193 import std.uni: toUpper, isAlpha, isAlphaNum; 194 195 string ret; 196 enum Action { 197 nothing, 198 capitalize, 199 } 200 auto action = Action.nothing; 201 202 if (name[0].isAlpha || name[0] == '_') { // record first character only if valid first character 203 ret ~= name[0]; 204 } 205 for (int i = 1; i < name.length; ++i) { 206 if (!name[i].isAlphaNum && name[0] != '_') { 207 action = Action.capitalize; 208 continue; 209 } 210 211 final switch (action) { 212 case Action.nothing: 213 ret ~= name[i]; 214 break; 215 case Action.capitalize: 216 ret ~= toUpper(name[i]); 217 break; 218 } 219 220 action = Action.nothing; 221 } 222 return ret; 223 } 224 225 unittest { 226 static assert(makeValidCamelCase!"a-a" == "aA"); 227 static assert(makeValidCamelCase!"-a" == "a"); 228 static assert(makeValidCamelCase!"-a-" == "a"); 229 static assert(makeValidCamelCase!"9a" == "a"); 230 static assert(makeValidCamelCase!"_9a" == "_9a"); 231 } 232 233 private struct CommandImpl( 234 string _name, 235 string _description = null, 236 _Args = Void, 237 alias _handler = null, 238 ) { 239 240 alias Name = _name; 241 alias Identifier = makeValidCamelCase!Name; 242 alias Description = _description; 243 alias Args = _Args; 244 alias Handler = _handler; 245 246 public alias description(string value) = CommandImpl!(Name, value, Args, Handler); 247 248 public static template args(U) if (isProgramOptions!U || isProgramCommands!U) { 249 alias args = CommandImpl!(Name, Description, U, Handler); 250 } 251 252 public static template handler(alias value) { 253 alias handler = CommandImpl!(Name, Description, Args, value); 254 } 255 256 private bool active = false; 257 public T opCast(T: bool)() { 258 return active; 259 } 260 public Args _args; 261 alias _args this; 262 } 263 264 /** 265 Represents one program command which identified the expected string on the command line. One of more of these can be given to 266 a `ProgramCommands` object as template arguments. 267 268 The `Command` object is accessible from a `ProgramCommands` object via the name given to the command. 269 270 If the name is not a valid identifier, it is transformed in to camel case by the following rules: 271 <ol> 272 <li> if first character is invalid first character for identifier, it is skipped. 273 <li> if any following character is not a valid identifier character, it is skipped AND the next valid character 274 is capitalized. 275 </ol> 276 277 Params: 278 name = The name of the command. 279 280 Named_optional_arguments: 281 282 A number of named optional arguments can be given to a `Command` for e.g.: 283 --- 284 ProgramCommands!( 285 Command!"push".description!"This command pushes all your bases belongs to us" 286 ); 287 --- 288 289 This will create a commands object that can parse a command `push` on the command line. 290 291 The following named optional arguments are available: 292 293 <li>`description`: `string` - description for help message 294 <li>`args`: $(DDOX_NAMED_REF dcli.program_options, `ProgramOptions`)|`ProgramCommands` - this can be given a set of program 295 options that can be used with this command, or it can be given a program commands object so that it has sub commands 296 <li>`handler`: `void function(T)` - this is a handler function that will only be called if this command was present on the 297 command line. The type `T` passed in will be your `ProgramCommands` structure instance 298 */ 299 public template Command(string name) if (name.length > 0) { 300 alias Command = CommandImpl!name; 301 } 302 303 /* 304 These two functions are here so because both ProgramCommands and ProgramOptions adhere to an interface that 305 has these two functions. And when a Command is created that is only a command with no inner options or 306 commands, then it resolves to type Void. So these function "fake" a similar interface for Void. 307 */ 308 private void parse(Void, const string[]) {} 309 private string toString(Void) { return ""; } 310 311 /** 312 You can configure a `ProgramCommands` object with a number of `Command`s and then use it to parse 313 an list of command line arguments 314 315 The object will generate its member variables from the `Command`s you pass in, for e.g. 316 317 --- 318 auto commands = ProgramCommands!(Command!"one", Command!"two"); 319 commands.one // generated variable 320 commands.two // generated variable 321 --- 322 323 After you parse command line arguments, the commands that are encountered on the command line become 324 "activated" and can be checked by casting them to a boolean, i.e. 325 326 --- 327 commands.parse(["two"]); 328 if (commands.one) {} // will be false 329 if (commands.two) {} // will be true 330 --- 331 332 You can also assign "handler" to a command and use the `executeHandlers` function to call them for the 333 commands that were activated. 334 335 Params: 336 Commands = 1 or more `Command` objects, each representing one command line argument. The first 337 argument may be a `ProgramOptions` type if you want the command to have a set of options 338 that it may handle 339 */ 340 public struct ProgramCommands(Commands...) if (Commands.length > 0) { 341 import std.conv: to; 342 343 static if (isProgramOptions!(Commands[0])) { 344 // We have a global set of options if the first argument is a ProgramOptions type. 345 enum StartIndex = 1; 346 /** 347 The `ProgramOptions` that are associated with this command if any was passed in. If no `ProgramOptions` 348 where passed as an argument then this aliases to a `Void` pseudo type. 349 */ 350 public Commands[0] options; 351 } else { 352 enum StartIndex = 0; 353 public Void options; 354 } 355 356 // Mixin the variables for each command 357 static foreach (I; StartIndex .. Commands.length) { 358 static assert( 359 isCommand!(Commands[I]), 360 "Expected type Command. Found " ~ Commands[I].stringof ~ " for arg " ~ I.to!string 361 ); 362 mixin("public Commands[I] " ~ Commands[I].Identifier ~ ";"); 363 } 364 365 // Get all available commands 366 private immutable allCommands = () { 367 string[] result; 368 static foreach (I; StartIndex .. Commands.length) { 369 result ~= Commands[I].Name; 370 } 371 return result; 372 }(); 373 374 private void parseCommand(string cmd, const string[] args) { 375 command: switch (cmd) { 376 static foreach (I; StartIndex .. Commands.length) { 377 mixin(`case "` ~ Commands[I].Name ~ `":`); 378 mixin(Commands[I].Identifier ~ ".parse(args);"); 379 break command; 380 } 381 default: 382 debug_print("cannot parse invalid command: " ~ cmd); 383 break; 384 } 385 } 386 387 // Sets a command to true for when it was encountered 388 private void activateCommand(string cmd) { 389 command: switch (cmd) { 390 static foreach (I; StartIndex .. Commands.length) { 391 mixin(`case "` ~ Commands[I].Name ~ `":`); 392 mixin(Commands[I].Identifier ~ ".active = true;"); 393 break command; 394 } 395 default: 396 debug_print("cannot activate invalid command: " ~ cmd); 397 break; 398 } 399 } 400 401 // Returns trur if a string is a valid command 402 private bool isValidCommand(string cmd) { 403 import std.algorithm: canFind; 404 return allCommands.canFind(cmd); 405 } 406 407 private alias PluckCommandResult = Tuple!(const(string)[], string, const(string)[]); 408 private PluckCommandResult pluckCommand(const string[] args) { 409 alias pred = (a) => this.isValidCommand(a); 410 return args.indexWhere!pred 411 .map!((i) { 412 return PluckCommandResult( 413 args[0 .. i], 414 args[i], 415 i > 0 ? args[i + 1 .. $] : [], 416 ); 417 }) 418 .frontOr(PluckCommandResult.init); 419 } 420 421 /** 422 Parses the command line arguments according to the set of `Commands`s that are passed in. 423 */ 424 public void parse(const string[] args) { 425 auto data = pluckCommand(args); 426 427 debug_print("plucked => ", data); 428 429 if (data[0].length) { 430 this.options.parse(data[0]); 431 } 432 433 if (data[1].length) { 434 activateCommand(data[1]); 435 parseCommand(data[1], data[2]); 436 } 437 } 438 439 /** 440 Returns a string that represents a block of text that can be output to stdout 441 to display a help message 442 */ 443 public string helpText() const { 444 string ret; 445 static if (isProgramOptions!(Commands[0])) { 446 ret ~= this.options.helpText; 447 ret ~= "\n"; 448 } 449 static if (StartIndex < Commands.length) 450 ret ~= "Commands:\n"; 451 static foreach (I; StartIndex .. Commands.length) { 452 ret ~= " " ~ Commands[I].Name; 453 if (Commands[I].Description.length) 454 ret ~= " " ~ Commands[I].Description 455 ; 456 static if (I < Commands.length - 1) 457 ret ~= "\n"; 458 } 459 return ret; 460 } 461 462 /** 463 Returns a string that is a stringified object of keys and values denoting commands 464 and their values and options (if present) 465 */ 466 public string toString() const { 467 import std.conv: to; 468 string ret = "{ "; 469 static if (isProgramOptions!(Commands[0])) { 470 ret ~= "options: " ~ this.options.toString ~ ", "; 471 } 472 static foreach (I; StartIndex .. Commands.length) { 473 ret ~= Commands[I].Name ~ ": { "; 474 ret ~= "active: " ~ mixin(Commands[I].Identifier ~ ".active ? \"true\" : \"false\""); 475 ret ~= ", "; 476 static if (isProgramOptions!(Commands[I].Args)) { 477 ret ~= "options"; 478 } else { 479 ret ~= "commands"; 480 } 481 ret ~= ": " ~ mixin(Commands[I].Identifier ~ ".toString"); 482 ret ~= " }"; 483 static if (I < Commands.length - 1) { 484 ret ~= ", "; 485 } 486 } 487 ret ~= " }"; 488 return ret; 489 } 490 491 /** 492 If any of the `Command`s have any handlers specified. Then those will be called 493 in order of appearance on the command line by calling this function 494 */ 495 public void executeHandlers() { 496 import bolts: isNullType; 497 static foreach (I; StartIndex .. Commands.length) { 498 if (mixin(Commands[I].Identifier)) { // if this command is active 499 static if (!isNullType!(Commands[I].Handler)) { 500 Commands[I].Handler(this); 501 } 502 static if (isProgramCommands!(Commands[I].Args)) { 503 mixin(Commands[I].Identifier ~ ".executeHandlers();"); 504 } 505 } 506 } 507 } 508 } 509 510 version (unittest) { 511 struct Fixtures { 512 static: 513 514 private bool handledCommand2 = false; 515 private bool handledCommand3 = false; 516 private bool handledCommand3Sub1 = false; 517 518 bool checkResetHandledCommand2() { 519 const ret = handledCommand2; 520 scope(exit) handledCommand2 = false; 521 return ret; 522 } 523 bool checkResetHandledCommand3() { 524 const ret = handledCommand3; 525 scope(exit) handledCommand3 = false; 526 return ret; 527 } 528 bool checkResetHandledCommand3Sub1() { 529 const ret = handledCommand3Sub1; 530 scope(exit) handledCommand3Sub1 = false; 531 return ret; 532 } 533 534 void handleCommand2(T)(T command) { 535 handledCommand2 = true; 536 } 537 538 void handleCommand3(T)(T command) { 539 handledCommand3 = true; 540 } 541 542 void handleCommand3Sub1(T)(T command) { 543 handledCommand3Sub1 = true; 544 } 545 } 546 } 547 548 unittest { 549 auto commands = ProgramCommands!(Command!"dashed-command")(); 550 commands.parse(["dashed-command"]); 551 assert(cast(bool)commands.dashedCommand); 552 }