1 module jcli.helptext.helptext; 2 3 import jcli.core, jcli.text, jcli.introspect, std; 4 5 struct CommandHelpText(alias CommandT_) 6 { 7 alias CommandT = CommandT_; 8 alias CommandInfo = commandInfoFor!CommandT; 9 10 private string _cached; 11 12 string generate(string appName = null, size_t width = 180) 13 { 14 if(this._cached) 15 return this._cached; 16 17 HelpText help = HelpText.make(width); 18 19 if(appName is null) 20 appName = thisExePath().baseName; 21 22 static struct Arg 23 { 24 string name; 25 string description; 26 ArgGroup group; 27 bool optional; 28 } 29 30 string patternToNamedArgList(Pattern pattern) 31 { 32 return pattern.patterns 33 .map!(p => p.length == 1 ? "-"~p : "--"~p) 34 .fold!((a,b) => a.length ? a~"|"~b : b)(""); 35 } 36 37 Arg[] positionals; 38 Arg[] named; 39 40 static foreach(i, pos; CommandInfo.positionalArgs) 41 positionals ~= Arg(pos.uda.name, pos.uda.description); 42 static foreach(i, nam; CommandInfo.namedArgs) 43 { 44 named ~= Arg( 45 patternToNamedArgList(nam.uda.pattern), 46 nam.uda.description, 47 nam.group, 48 !!(nam.existence & ArgExistence.optional) 49 ); 50 } 51 52 named.multiSort!("a.optional != b.optional", "a.name < b.name"); 53 54 help.addLineWithPrefix("Usage: ", "%s %s%s%s".format( 55 appName, 56 CommandInfo.pattern.patterns.walkLength ? CommandInfo.pattern.patterns.front : "DEFAULT", 57 positionals.map!(p => "<"~p.name~">").fold!((a,b) => a~" "~b)(""), 58 named.map!(p => p.optional ? "["~p.name~"]" : p.name).fold!((a,b) => a~" "~b)("") 59 ), AnsiStyleSet.init.style(AnsiStyle.init.bold).nullable); 60 help.addLine(null); 61 help.addLine(null); 62 63 if(CommandInfo.description) 64 help.addHeaderWithText("Description:", CommandInfo.description); 65 66 help.addHeader("Positional Arguments:"); 67 foreach(pos; positionals) 68 { 69 help.addArgument( 70 pos.name, 71 [HelpTextDescription(0, pos.description)] 72 ); 73 } 74 75 Arg[][ArgGroup] argsByGroup; 76 foreach(nam; named) 77 { 78 scope ptr = (nam.group in argsByGroup); 79 if(!ptr) 80 { 81 argsByGroup[nam.group] = Arg[].init; 82 ptr = (nam.group in argsByGroup); 83 } 84 85 (*ptr) ~= nam; 86 } 87 88 foreach(group, args; argsByGroup) 89 { 90 help.addLine(null); 91 92 if(group == ArgGroup.init) 93 help.addHeader("Named Arguments:"); 94 else if(group.description == null) 95 help.addHeader(group.name); 96 else 97 help.addHeaderWithText(group.name, group.description); 98 99 foreach(arg; args) 100 { 101 auto descs = [HelpTextDescription(0, arg.description)]; 102 103 help.addArgument( 104 arg.name, 105 descs 106 ); 107 } 108 } 109 110 this._cached = help.finish(); 111 return this._cached; 112 } 113 } 114 115 unittest 116 { 117 @Command("command", "This is a command that is totally super complicated.") 118 static struct ComplexCommand 119 { 120 @ArgPositional("arg1", "This is a generic argument that isn't grouped anywhere") 121 int a; 122 @ArgPositional("arg2", "This is a generic argument that isn't grouped anywhere") 123 int b; 124 @ArgPositional("output", "Where to place the output.") 125 string output; 126 127 @ArgNamed("test-flag", "Test flag, please ignore.") 128 bool flag; 129 130 @ArgGroup("Debug", "Arguments related to debugging.") 131 { 132 @ArgNamed("verbose|v", "Enables verbose logging.") 133 Nullable!bool verbose; 134 135 @ArgNamed("log|l", "Specifies a log file to direct output to.") 136 Nullable!string log; 137 } 138 139 @ArgGroup("I/O", "Arguments related to I/O.") 140 @ArgNamed("config|c", "Specifies the config file to use.") 141 Nullable!string config; 142 143 void onExecute(){} 144 } 145 146 auto c = CommandHelpText!ComplexCommand(); 147 // I've learned its next to pointless to fully unittest help text, since it can change so subtly and so often 148 // that manual validation is good enough. 149 //assert(false, c.generate()); 150 }