1 module jcli.helptext.helptext; 2 3 import jcli.core, jcli.text, jcli.introspect; 4 5 // TODO: Maybe make it take the processed command info instead of the command type itself. 6 struct CommandHelpText(alias CommandT_) 7 { 8 alias CommandType = CommandT_; 9 alias CommandInfo = jcli.introspect.CommandInfo!CommandType; 10 alias ArgumentInfo = CommandInfo.Arguments; 11 12 private string _cached; 13 14 import std.file : thisExePath; 15 import std.path : baseName; 16 17 string generate(string appName = thisExePath().baseName, uint width = 180) 18 { 19 import std.range; 20 import std.algorithm; 21 22 if (this._cached) 23 return this._cached; 24 25 HelpText help = HelpText.make(width); 26 27 static struct Arg 28 { 29 string name; 30 string description; 31 ArgGroup group; 32 bool optional; 33 } 34 35 static string patternToNamedArgList(Pattern pattern) 36 { 37 return pattern.map!(p => p.length == 1 ? "-" ~ p : "--" ~ p).join(" "); 38 } 39 40 Arg[] positionals; 41 Arg[] named; 42 43 static foreach(i, pos; ArgumentInfo.positional) 44 positionals ~= Arg(pos.uda.name, pos.uda.description); 45 static foreach(i, nam; ArgumentInfo.named) 46 { 47 named ~= Arg( 48 patternToNamedArgList(cast()nam.pattern), 49 nam.description, 50 nam.group, 51 nam.flags.has(ArgFlags._optionalBit) 52 ); 53 } 54 55 named.multiSort!("a.optional != b.optional", "a.name < b.name"); 56 57 static if (CommandInfo.flags.hasEither(CommandFlags.explicitlyDefault | CommandFlags.stringAttribute)) 58 enum name = "Default"; 59 else 60 enum name = CommandInfo.udaValue.name; 61 62 // TODO: 63 // This allocates way too much memory for no reason, and is slower as a result. 64 // Just make that addLineWithPrefix take a range, and just chain these together. 65 // Or make it expose the appender and do a `formattedWrite`. 66 import std.format : format; 67 help.addLineWithPrefix("Usage: ", "%s %s%s %s".format( 68 appName, 69 name, 70 positionals 71 .map!(p => "<" ~ p.name ~ ">") 72 .join(" "), 73 named 74 .map!(p => p.optional ? "[" ~ p.name ~ "]" : p.name) 75 .join(" ") 76 ), AnsiStyleSet.init.style(AnsiStyle.init.bold)); 77 78 static if (CommandInfo.description) 79 help.addHeaderWithText("Description: ", CommandInfo.description); 80 81 if (positionals.length > 0) 82 { 83 help.addHeader("Positional Arguments:"); 84 foreach (pos; positionals) 85 { 86 help.addArgument( 87 pos.name, 88 [HelpTextDescription(0, pos.description)] 89 ); 90 } 91 } 92 93 Arg[][ArgGroup] argsByGroup; 94 foreach(nam; named) 95 { 96 scope ptr = (nam.group in argsByGroup); 97 if (!ptr) 98 { 99 argsByGroup[nam.group] = Arg[].init; 100 ptr = (nam.group in argsByGroup); 101 } 102 103 (*ptr) ~= nam; 104 } 105 106 foreach (group, args; argsByGroup) 107 { 108 help.addLine(null); 109 110 if (group == ArgGroup.init) 111 help.addHeader("Named Arguments: "); 112 else if (group.description == null) 113 help.addHeader(group.name); 114 else 115 help.addHeaderWithText(group.name, group.description); 116 117 foreach (arg; args) 118 { 119 auto descs = [HelpTextDescription(0, arg.description)]; 120 121 help.addArgument( 122 arg.name, 123 descs 124 ); 125 } 126 } 127 128 this._cached = help.finish(); 129 return this._cached; 130 } 131 } 132 133 unittest 134 { 135 @Command("command", "This is a command that is totally super complicated.") 136 static struct ComplexCommand 137 { 138 @ArgPositional("arg1", "This is a generic argument that isn't grouped anywhere") 139 int a; 140 @ArgPositional("arg2", "This is a generic argument that isn't grouped anywhere") 141 int b; 142 @ArgPositional("output", "Where to place the output.") 143 string output; 144 145 @ArgNamed("test-flag", "Test flag, please ignore.") 146 @(ArgConfig.parseAsFlag) 147 bool flag; 148 149 @ArgGroup("Debug", "Arguments related to debugging.") 150 { 151 @ArgNamed("verbose|v", "Enables verbose logging.") 152 Nullable!bool verbose; 153 154 @ArgNamed("log|l", "Specifies a log file to direct output to.") 155 Nullable!string log; 156 } 157 158 @ArgGroup("I/O", "Arguments related to I/O.") 159 @ArgNamed("config|c", "Specifies the config file to use.") 160 Nullable!string config; 161 162 void onExecute(){} 163 } 164 165 auto c = CommandHelpText!ComplexCommand(); 166 // I've learned its next to pointless to fully unittest help text, since it can change so subtly and so often 167 // that manual validation is good enough. 168 //assert(false, c.generate()); 169 }