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, uint 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)); 60 61 if(CommandInfo.description) 62 help.addHeaderWithText("Description:", CommandInfo.description); 63 64 if(positionals.length) 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 76 Arg[][ArgGroup] argsByGroup; 77 foreach(nam; named) 78 { 79 scope ptr = (nam.group in argsByGroup); 80 if(!ptr) 81 { 82 argsByGroup[nam.group] = Arg[].init; 83 ptr = (nam.group in argsByGroup); 84 } 85 86 (*ptr) ~= nam; 87 } 88 89 foreach(group, args; argsByGroup) 90 { 91 help.addLine(null); 92 93 if(group == ArgGroup.init) 94 help.addHeader("Named Arguments:"); 95 else if(group.description == null) 96 help.addHeader(group.name); 97 else 98 help.addHeaderWithText(group.name, group.description); 99 100 foreach(arg; args) 101 { 102 auto descs = [HelpTextDescription(0, arg.description)]; 103 104 help.addArgument( 105 arg.name, 106 descs 107 ); 108 } 109 } 110 111 this._cached = help.finish(); 112 return this._cached; 113 } 114 } 115 116 unittest 117 { 118 @Command("command", "This is a command that is totally super complicated.") 119 static struct ComplexCommand 120 { 121 @ArgPositional("arg1", "This is a generic argument that isn't grouped anywhere") 122 int a; 123 @ArgPositional("arg2", "This is a generic argument that isn't grouped anywhere") 124 int b; 125 @ArgPositional("output", "Where to place the output.") 126 string output; 127 128 @ArgNamed("test-flag", "Test flag, please ignore.") 129 bool flag; 130 131 @ArgGroup("Debug", "Arguments related to debugging.") 132 { 133 @ArgNamed("verbose|v", "Enables verbose logging.") 134 Nullable!bool verbose; 135 136 @ArgNamed("log|l", "Specifies a log file to direct output to.") 137 Nullable!string log; 138 } 139 140 @ArgGroup("I/O", "Arguments related to I/O.") 141 @ArgNamed("config|c", "Specifies the config file to use.") 142 Nullable!string config; 143 144 void onExecute(){} 145 } 146 147 auto c = CommandHelpText!ComplexCommand(); 148 // I've learned its next to pointless to fully unittest help text, since it can change so subtly and so often 149 // that manual validation is good enough. 150 //assert(false, c.generate()); 151 }