1 module argparse.help; 2 3 import argparse; 4 import argparse.internal; 5 6 /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 7 /// Help printing functions 8 /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 9 10 package enum helpArgument = { 11 ArgumentInfo arg; 12 arg.names = ["h","help"]; 13 arg.description = "Show this help message and exit"; 14 arg.minValuesCount = 0; 15 arg.maxValuesCount = 0; 16 arg.allowBooleanNegation = false; 17 arg.ignoreInDefaultCommand = true; 18 return arg; 19 }(); 20 21 private bool isHelpArgument(string name) 22 { 23 static foreach(n; helpArgument.names) 24 if(n == name) 25 return true; 26 27 return false; 28 } 29 30 unittest 31 { 32 assert(isHelpArgument("h")); 33 assert(isHelpArgument("help")); 34 assert(!isHelpArgument("a")); 35 assert(!isHelpArgument("help1")); 36 } 37 38 /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 39 40 package string getProgramName() 41 { 42 import core.runtime: Runtime; 43 import std.path: baseName; 44 return Runtime.args[0].baseName; 45 } 46 47 unittest 48 { 49 assert(getProgramName().length > 0); 50 } 51 52 /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 53 54 package void substituteProg(Output)(auto ref Output output, string text, string prog) 55 { 56 import std.array: replaceInto; 57 output.replaceInto(text, "%(PROG)", prog); 58 } 59 60 unittest 61 { 62 import std.array: appender; 63 auto a = appender!string; 64 a.substituteProg("this is some text where %(PROG) is substituted but PROG and prog are not", "-myprog-"); 65 assert(a[] == "this is some text where -myprog- is substituted but PROG and prog are not"); 66 } 67 68 /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 69 70 package string spaces(ulong num) 71 { 72 import std.range: repeat; 73 import std.array: array; 74 return ' '.repeat(num).array; 75 } 76 77 unittest 78 { 79 assert(spaces(0) == ""); 80 assert(spaces(1) == " "); 81 assert(spaces(5) == " "); 82 } 83 84 /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 85 86 package void wrapMutiLine(Output, S)(auto ref Output output, 87 S s, 88 in size_t columns = 80, 89 S firstindent = null, 90 S indent = null, 91 in size_t tabsize = 8) 92 { 93 import std.string: wrap, lineSplitter, join; 94 import std.algorithm: map, copy; 95 96 auto lines = s.lineSplitter; 97 if(lines.empty) 98 { 99 output.put(firstindent); 100 output.put("\n"); 101 return; 102 } 103 104 output.put(lines.front.wrap(columns, firstindent, indent, tabsize)); 105 lines.popFront; 106 107 lines.map!(s => s.wrap(columns, indent, indent, tabsize)).copy(output); 108 } 109 110 unittest 111 { 112 string test(string s, size_t columns, string firstindent = null, string indent = null) 113 { 114 import std.array: appender; 115 auto a = appender!string; 116 a.wrapMutiLine(s, columns, firstindent, indent); 117 return a[]; 118 } 119 assert(test("a short string", 7) == "a short\nstring\n"); 120 assert(test("a\nshort string", 7) == "a\nshort\nstring\n"); 121 122 // wrap will not break inside of a word, but at the next space 123 assert(test("a short string", 4) == "a\nshort\nstring\n"); 124 125 assert(test("a short string", 7, "\t") == "\ta\nshort\nstring\n"); 126 assert(test("a short string", 7, "\t", " ") == "\ta\n short\n string\n"); 127 } 128 129 /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 130 131 private void printValue(Output)(auto ref Output output, in ArgumentInfo info) 132 { 133 if(info.maxValuesCount.get == 0) 134 return; 135 136 if(info.minValuesCount.get == 0) 137 output.put('['); 138 139 output.put(info.placeholder); 140 if(info.maxValuesCount.get > 1) 141 output.put(" ..."); 142 143 if(info.minValuesCount.get == 0) 144 output.put(']'); 145 } 146 147 unittest 148 { 149 auto test(int min, int max) 150 { 151 ArgumentInfo info; 152 info.placeholder = "v"; 153 info.minValuesCount = min; 154 info.maxValuesCount = max; 155 156 import std.array: appender; 157 auto a = appender!string; 158 a.printValue(info); 159 return a[]; 160 } 161 162 assert(test(0,0) == ""); 163 assert(test(0,1) == "[v]"); 164 assert(test(0,5) == "[v ...]"); 165 assert(test(1,1) == "v"); 166 assert(test(1,5) == "v ..."); 167 assert(test(3,3) == "v ..."); 168 assert(test(3,5) == "v ..."); 169 } 170 171 /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 172 173 private void printInvocation(Output)(auto ref Output output, in ArgumentInfo info, in string[] names, in Config config) 174 { 175 if(info.positional) 176 output.printValue(info); 177 else 178 { 179 import std.algorithm: each; 180 181 names.each!((i, name) 182 { 183 if(i > 0) 184 output.put(", "); 185 186 output.put(getArgumentName(name, config)); 187 188 if(info.maxValuesCount.get > 0) 189 { 190 output.put(' '); 191 output.printValue(info); 192 } 193 }); 194 } 195 } 196 197 unittest 198 { 199 auto test(bool positional)() 200 { 201 enum info = { 202 ArgumentInfo info; 203 info.placeholder = "v"; 204 static if (positional) 205 info.position = 0; 206 return info; 207 }(); 208 209 import std.array: appender; 210 auto a = appender!string; 211 a.printInvocation(info.setDefaults!(int, "foo"), ["f","foo"], Config.init); 212 return a[]; 213 } 214 215 assert(test!false == "-f v, --foo v"); 216 assert(test!true == "v"); 217 } 218 219 /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 220 221 private void printUsage(Output)(auto ref Output output, in ArgumentInfo info, in Config config) 222 { 223 if(!info.required) 224 output.put('['); 225 226 output.printInvocation(info, [info.names[0]], config); 227 228 if(!info.required) 229 output.put(']'); 230 } 231 232 unittest 233 { 234 auto test(bool required, bool positional)() 235 { 236 enum info = { 237 ArgumentInfo info; 238 info.names ~= "foo"; 239 info.placeholder = "v"; 240 info.required = required; 241 static if (positional) 242 info.position = 0; 243 return info; 244 }(); 245 246 import std.array: appender; 247 auto a = appender!string; 248 a.printUsage(info.setDefaults!(int, "foo"), Config.init); 249 return a[]; 250 } 251 252 assert(test!(false, false) == "[--foo v]"); 253 assert(test!(false, true) == "[v]"); 254 assert(test!(true, false) == "--foo v"); 255 assert(test!(true, true) == "v"); 256 } 257 258 /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 259 260 private void printUsage(T, Output)(auto ref Output output, in CommandArguments!T cmd, in Config config) 261 { 262 import std.algorithm: map; 263 import std.array: join; 264 265 string progName = (cmd.parentNames ~ cmd.info.names[0]).map!(_ => _.length > 0 ? _ : getProgramName()).join(" "); 266 267 output.put("Usage: "); 268 269 if(cmd.info.usage.length > 0) 270 substituteProg(output, cmd.info.usage, progName); 271 else 272 { 273 import std.algorithm: filter, each, map; 274 275 alias print = (r) => r 276 .filter!((ref _) => !_.hideFromHelp) 277 .each!((ref _) 278 { 279 output.put(' '); 280 argparse.help.printUsage(output, _, config); 281 }); 282 283 output.put(progName); 284 285 // named args 286 print(cmd.arguments.arguments.filter!((ref _) => !_.positional)); 287 // positional args 288 print(cmd.arguments.positionalArguments.map!(ref (_) => cmd.arguments.arguments[_])); 289 // sub commands 290 if(cmd.subCommands.length > 0) 291 output.put(" <command> [<args>]"); 292 } 293 294 output.put('\n'); 295 } 296 297 /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 298 299 private void printUsage(T, Output)(auto ref Output output, in Config config) 300 { 301 printUsage(output, CommandArguments!T(config), config); 302 } 303 304 unittest 305 { 306 @(Command("MYPROG").Usage("custom usage of %(PROG)")) 307 struct T 308 { 309 string s; 310 } 311 312 auto test(string usage) 313 { 314 import std.array: appender; 315 316 auto a = appender!string; 317 a.printUsage!T(Config.init); 318 return a[]; 319 } 320 321 enum expected = "Usage: custom usage of MYPROG\n"; 322 static assert(test("custom usage of %(PROG)") == expected); 323 assert(test("custom usage of %(PROG)") == expected); 324 } 325 326 /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 327 328 private void printHelp(Output, ARGS)(auto ref Output output, in Group group, ARGS args, int helpPosition) 329 { 330 import std.string: leftJustify; 331 332 if(group.arguments.length == 0 || group.name.length == 0) 333 return; 334 335 alias printDescription = { 336 output.put(group.name); 337 output.put(":\n"); 338 339 if (group.description.length > 0) 340 { 341 output.put(" "); 342 output.put(group.description); 343 output.put("\n\n"); 344 } 345 }; 346 bool descriptionIsPrinted = false; 347 348 immutable ident = spaces(helpPosition + 2); 349 350 foreach(idx; group.arguments) 351 { 352 auto arg = &args[idx]; 353 354 if(arg.invocation.length == 0) 355 continue; 356 357 if(!descriptionIsPrinted) 358 { 359 printDescription(); 360 descriptionIsPrinted = true; 361 } 362 363 if(arg.invocation.length <= helpPosition - 4) // 2=indent, 2=two spaces between invocation and help text 364 { 365 import std.array: appender; 366 367 auto invocation = appender!string; 368 invocation ~= " "; 369 invocation ~= arg.invocation.leftJustify(helpPosition); 370 output.wrapMutiLine(arg.help, 80-2, invocation[], ident); 371 } 372 else 373 { 374 // long action name; start on the next line 375 output.put(" "); 376 output.put(arg.invocation); 377 output.put("\n"); 378 output.wrapMutiLine(arg.help, 80-2, ident, ident); 379 } 380 } 381 382 output.put('\n'); 383 } 384 385 386 private void printHelp(Output)(auto ref Output output, in Arguments arguments, in Config config, bool helpArgIsPrinted = false) 387 { 388 import std.algorithm: map, maxElement, min; 389 import std.array: appender, array; 390 391 // pre-compute the output 392 auto args = 393 arguments.arguments 394 .map!((ref _) 395 { 396 struct Result 397 { 398 string invocation, help; 399 } 400 401 if(_.hideFromHelp) 402 return Result.init; 403 404 if(isHelpArgument(_.names[0])) 405 { 406 if(helpArgIsPrinted) 407 return Result.init; 408 409 helpArgIsPrinted = true; 410 } 411 412 auto invocation = appender!string; 413 invocation.printInvocation(_, _.names, config); 414 415 return Result(invocation[], _.description); 416 }).array; 417 418 immutable maxInvocationWidth = args.map!(_ => _.invocation.length).maxElement; 419 immutable helpPosition = min(maxInvocationWidth + 4, 24); 420 421 //user-defined groups 422 foreach(ref group; arguments.groups[2..$]) 423 output.printHelp(group, args, helpPosition); 424 425 //required args 426 output.printHelp(arguments.requiredGroup, args, helpPosition); 427 428 //optionals args 429 output.printHelp(arguments.optionalGroup, args, helpPosition); 430 431 if(arguments.parentArguments) 432 output.printHelp(*arguments.parentArguments, config, helpArgIsPrinted); 433 } 434 435 private void printHelp(Output)(auto ref Output output, in CommandInfo[] commands, in Config config) 436 { 437 import std.algorithm: map, maxElement, min; 438 import std.array: appender, array, join; 439 440 if(commands.length == 0) 441 return; 442 443 output.put("Available commands:\n"); 444 445 // pre-compute the output 446 auto cmds = commands 447 .map!((ref _) 448 { 449 struct Result 450 { 451 string invocation, help; 452 } 453 454 //if(_.hideFromHelp) 455 // return Result.init; 456 457 return Result(_.names.join(","), _.shortDescription.length > 0 ? _.shortDescription : _.description); 458 }).array; 459 460 immutable maxInvocationWidth = cmds.map!(_ => _.invocation.length).maxElement; 461 immutable helpPosition = min(maxInvocationWidth + 4, 24); 462 463 464 immutable ident = spaces(helpPosition + 2); 465 466 foreach(const ref cmd; cmds) 467 { 468 if(cmd.invocation.length == 0) 469 continue; 470 471 if(cmd.invocation.length <= helpPosition - 4) // 2=indent, 2=two spaces between invocation and help text 472 { 473 import std.array: appender; 474 import std.string: leftJustify; 475 476 auto invocation = appender!string; 477 invocation ~= " "; 478 invocation ~= cmd.invocation.leftJustify(helpPosition); 479 output.wrapMutiLine(cmd.help, 80-2, invocation[], ident); 480 } 481 else 482 { 483 // long action name; start on the next line 484 output.put(" "); 485 output.put(cmd.invocation); 486 output.put("\n"); 487 output.wrapMutiLine(cmd.help, 80-2, ident, ident); 488 } 489 } 490 491 output.put('\n'); 492 } 493 494 495 private void printHelp(T, Output)(auto ref Output output, in CommandArguments!T cmd, in Config config) 496 { 497 printUsage(output, cmd, config); 498 output.put('\n'); 499 500 if(cmd.info.description.length > 0) 501 { 502 output.put(cmd.info.description); 503 output.put("\n\n"); 504 } 505 506 // sub commands 507 output.printHelp(cmd.subCommands, config); 508 509 output.printHelp(cmd.arguments, config); 510 511 if(cmd.info.epilog.length > 0) 512 { 513 output.put(cmd.info.epilog); 514 output.put('\n'); 515 } 516 } 517 518 void printHelp(T, Output)(auto ref Output output, in Config config) 519 { 520 printHelp(output, CommandArguments!T(config), config); 521 } 522 523 unittest 524 { 525 @(Command("MYPROG") 526 .Description("custom description") 527 .Epilog("custom epilog") 528 ) 529 struct T 530 { 531 @NamedArgument string s; 532 @(NamedArgument.Placeholder("VALUE")) string p; 533 534 @(NamedArgument.HideFromHelp()) string hidden; 535 536 enum Fruit { apple, pear }; 537 @(NamedArgument(["f","fruit"]).Required().Description("This is a help text for fruit. Very very very very very very very very very very very very very very very very very very very long text")) Fruit f; 538 539 @(NamedArgument.AllowedValues!([1,4,16,8])) int i; 540 541 @(PositionalArgument(0).Description("This is a help text for param0. Very very very very very very very very very very very very very very very very very very very long text")) string param0; 542 @(PositionalArgument(1).AllowedValues!(["q","a"])) string param1; 543 544 @TrailingArguments string[] args; 545 } 546 547 auto test(alias func)() 548 { 549 import std.array: appender; 550 551 auto a = appender!string; 552 func!T(a, Config.init); 553 return a[]; 554 } 555 static assert(test!printUsage.length > 0); // ensure that it works at compile time 556 static assert(test!printHelp .length > 0); // ensure that it works at compile time 557 558 assert(test!printUsage == "Usage: MYPROG [-s S] [-p VALUE] -f {apple,pear} [-i {1,4,16,8}] [-h] param0 {q,a}\n"); 559 assert(test!printHelp == "Usage: MYPROG [-s S] [-p VALUE] -f {apple,pear} [-i {1,4,16,8}] [-h] param0 {q,a}\n\n"~ 560 "custom description\n\n"~ 561 "Required arguments:\n"~ 562 " -f {apple,pear}, --fruit {apple,pear}\n"~ 563 " This is a help text for fruit. Very very very very\n"~ 564 " very very very very very very very very very very\n"~ 565 " very very very very very long text\n"~ 566 " param0 This is a help text for param0. Very very very very\n"~ 567 " very very very very very very very very very very\n"~ 568 " very very very very very long text\n"~ 569 " {q,a} \n\n"~ 570 "Optional arguments:\n"~ 571 " -s S \n"~ 572 " -p VALUE \n"~ 573 " -i {1,4,16,8} \n"~ 574 " -h, --help Show this help message and exit\n\n"~ 575 "custom epilog\n"); 576 } 577 578 unittest 579 { 580 @Command("MYPROG") 581 struct T 582 { 583 @(ArgumentGroup("group1").Description("group1 description")) 584 { 585 @NamedArgument 586 { 587 string a; 588 string b; 589 } 590 @PositionalArgument(0) string p; 591 } 592 593 @(ArgumentGroup("group2").Description("group2 description")) 594 @NamedArgument 595 { 596 string c; 597 string d; 598 } 599 @PositionalArgument(1) string q; 600 } 601 602 auto test(alias func)() 603 { 604 import std.array: appender; 605 606 auto a = appender!string; 607 func!T(a, Config.init); 608 return a[]; 609 } 610 611 assert(test!printHelp == "Usage: MYPROG [-a A] [-b B] [-c C] [-d D] [-h] p q\n\n"~ 612 "group1:\n"~ 613 " group1 description\n\n"~ 614 " -a A \n"~ 615 " -b B \n"~ 616 " p \n\n"~ 617 "group2:\n"~ 618 " group2 description\n\n"~ 619 " -c C \n"~ 620 " -d D \n\n"~ 621 "Required arguments:\n"~ 622 " q \n\n"~ 623 "Optional arguments:\n"~ 624 " -h, --help Show this help message and exit\n\n"); 625 } 626 627 unittest 628 { 629 import std.sumtype: SumType; 630 631 @Command("MYPROG") 632 struct T 633 { 634 @(Command("cmd1").ShortDescription("Perform cmd 1")) 635 struct CMD1 636 { 637 string a; 638 } 639 @(Command("very-long-command-name-2").ShortDescription("Perform cmd 2")) 640 struct CMD2 641 { 642 string b; 643 } 644 645 string c; 646 string d; 647 648 SumType!(CMD1, CMD2) cmd; 649 } 650 651 auto test(alias func)() 652 { 653 import std.array: appender; 654 655 auto a = appender!string; 656 func!T(a, Config.init); 657 return a[]; 658 } 659 660 assert(test!printHelp == "Usage: MYPROG [-c C] [-d D] [-h] <command> [<args>]\n\n"~ 661 "Available commands:\n"~ 662 " cmd1 Perform cmd 1\n"~ 663 " very-long-command-name-2\n"~ 664 " Perform cmd 2\n\n"~ 665 "Optional arguments:\n"~ 666 " -c C \n"~ 667 " -d D \n"~ 668 " -h, --help Show this help message and exit\n\n"); 669 }