1 module commandr.help; 2 3 import commandr.program; 4 import commandr.option; 5 import std.algorithm : filter, map, any, chunkBy; 6 import std.array : join, array; 7 import std.conv : to; 8 import std.stdio : writefln, writeln, write; 9 import std..string : format; 10 import std.range : chain, empty; 11 12 13 /// 14 struct HelpOutput { 15 /// 16 bool colors = true; 17 // bool compact = false; 18 19 /// 20 // int maxWidth = 80; 21 22 /// 23 int indent = 24; 24 /// 25 int optionsLimit = 6; 26 /// 27 int commandLimit = 6; 28 } 29 30 /// 31 void printHelp(Command program, HelpOutput output = HelpOutput.init) { 32 HelpPrinter(output).printHelp(program); 33 } 34 35 /// 36 void printUsage(Command program, HelpOutput output = HelpOutput.init) { 37 HelpPrinter(output).printUsage(program); 38 } 39 40 41 struct HelpPrinter { 42 HelpOutput config; 43 44 public this(HelpOutput config) nothrow pure @safe { 45 this.config = config; 46 } 47 48 void printHelp(Command program) { 49 if (cast(Program)program) { 50 writefln("%s: %s %s(%s)%s", program.name, program.summary, ansi("2"), program.version_, ansi("0")); 51 writeln(); 52 } 53 else { 54 writefln("%s: %s", program.chain.join(" "), program.summary); 55 writeln(); 56 } 57 58 writefln("%sUSAGE%s", ansi("1"), ansi("0")); 59 write(" $ "); // prefix for usage 60 printUsage(program); 61 writeln(); 62 63 if (!program.flags.empty) { 64 writefln("%sFLAGS%s", ansi("1"), ansi("0")); 65 foreach(flag; program.flags) { 66 printHelp(flag); 67 } 68 writeln(); 69 } 70 71 if (!program.options.empty) { 72 writefln("%sOPTIONS%s", ansi("1"), ansi("0")); 73 foreach(option; program.options) { 74 printHelp(option); 75 } 76 writeln(); 77 } 78 79 if (!program.arguments.empty) { 80 writefln("%sARGUMENTS%s", ansi("1"), ansi("0")); 81 foreach(arg; program.arguments) { 82 printHelp(arg); 83 } 84 writeln(); 85 } 86 87 if (program.commands.length > 0) { 88 writefln("%sSUBCOMMANDS%s", ansi("1"), ansi("0")); 89 printSubcommands(program.commands); 90 writeln(); 91 } 92 } 93 94 void printUsage(Program program) { 95 string optionsUsage = "[options]"; 96 97 // if there are not too many options 98 if (program.options.length + program.flags.length <= config.optionsLimit) { 99 optionsUsage = chain( 100 program.flags.map!(f => optionUsage(f)), 101 program.options.map!(o => optionUsage(o)) 102 ).join(" "); 103 } else { 104 optionsUsage ~= " " ~ program.options.filter!(o => o.isRequired).map!(o => optionUsage(o)).join(" "); 105 } 106 107 string commands = program.commands.length == 0 ? "" : ( 108 program.commands.length > config.commandLimit ? "COMMAND" : program.commands.keys.join("|") 109 ); 110 string args = program.arguments.map!(a => argUsage(a)).join(" "); 111 112 writefln("%s %s %s%s", 113 program.binaryName, 114 optionsUsage, 115 args.empty ? "" : args ~ " ", 116 commands 117 ); 118 } 119 120 void printUsage(Command command) { 121 string optionsUsage = "[options]"; 122 if (command.options.length + command.flags.length <= config.optionsLimit) { 123 optionsUsage = chain( 124 command.flags.map!(f => optionUsage(f)), 125 command.options.map!(o => optionUsage(o)) 126 ).join(" "); 127 } else { 128 optionsUsage ~= " " ~ command.options.filter!(o => o.isRequired).map!(o => optionUsage(o)).join(" "); 129 } 130 131 string commands = command.commands.length == 0 ? "" : ( 132 command.commands.length > config.commandLimit ? "command" : command.commands.keys.join("|") 133 ); 134 string args = command.arguments.map!(a => argUsage(a)).join(" "); 135 136 writefln("%s %s %s%s", 137 usageChain(command), 138 optionsUsage, 139 args.empty ? "" : args ~ " ", 140 commands 141 ); 142 } 143 144 private void printHelp(Flag flag) { 145 string left = optionNames(flag); 146 writefln(" %-"~config.indent.to!string~"s %s%s%s", left, ansi("2"), flag.description, ansi("0")); 147 } 148 149 private void printHelp(Option option) { 150 string left = optionNames(option); 151 size_t length = left.length + option.tag.length + 1; 152 string formatted = "%s %s%s%s".format(left, ansi("4"), option.tag, ansi("0")); 153 size_t padLength = config.indent + (formatted.length - length); 154 155 writefln(" %-"~padLength.to!string~"s %s%s%s", formatted, ansi("2"), option.description, ansi("0")); 156 } 157 158 private void printHelp(Argument arg) { 159 writefln(" %-"~config.indent.to!string~"s %s%s%s", arg.tag, ansi("2"), arg.description, ansi("0")); 160 } 161 162 private void printSubcommands(Command[string] commands) { 163 auto grouped = commands.values.chunkBy!(a => a.topic).array; 164 165 if (grouped.length == 1 && grouped[0][0] is null) { 166 foreach(key, command; commands) { 167 writefln(" %-"~config.indent.to!string~"s %s%s%s", key, ansi("2"), command.summary, ansi("0")); 168 } 169 } 170 else { 171 foreach (entry; grouped) { 172 writefln(" %s%s%s:", ansi("4"), entry[0], ansi("0")); 173 foreach(command; entry[1]) { 174 writefln( 175 " %-"~(config.indent - 2).to!string~"s %s%s%s", 176 command.name, ansi("2"), command.summary, ansi("0") 177 ); 178 } 179 writeln(); 180 } 181 } 182 } 183 184 private string usageChain(Command target) { 185 Command[] commands = []; 186 Command dest = target.parent; 187 while (dest !is null) { 188 commands ~= dest; 189 dest = dest.parent; 190 } 191 192 string[] elements; 193 194 foreach_reverse(command; commands) { 195 elements ~= ansi("0") ~ command.name ~ ansi("2"); 196 197 foreach (opt; command.options.filter!(o => o.isRequired)) { 198 elements ~= optionUsage(opt) ~ ansi("2"); 199 } 200 201 foreach (arg; command.arguments.filter!(o => o.isRequired)) { 202 elements ~= argUsage(arg); 203 } 204 } 205 206 elements ~= ansi("0") ~ target.name; 207 208 return elements.join(" "); 209 } 210 211 private string optionNames(T)(T o) { 212 string names = ""; 213 214 if (o.abbrev) { 215 names ~= "-" ~ o.abbrev; 216 } 217 else { 218 names ~= " "; 219 } 220 221 if (o.full) { 222 if (o.abbrev) { 223 names ~= ", "; 224 } 225 names ~= "--%s".format(o.full); 226 } 227 228 return names; 229 } 230 231 private string optionUsage(IOption o) { 232 string result = o.displayName; 233 234 if (cast(Option)o) { 235 result = "%s %s%s%s".format(result, ansi("4"), (cast(Option)o).tag, ansi("0")); 236 } 237 238 if (!o.isRequired) { 239 result = "[%s]".format(result); 240 } 241 242 return result; 243 } 244 245 private string argUsage(Argument arg) { 246 return (arg.isRequired ? "%s" : "[%s]").format(arg.tag); 247 } 248 249 250 private string ansi(string code) { 251 version(Windows) { 252 return ""; 253 } 254 version(Posix) { 255 import core.sys.posix.unistd : isatty, STDOUT_FILENO; 256 257 if (config.colors && isatty(STDOUT_FILENO)) { 258 return "\033[%sm".format(code); 259 } 260 261 return ""; 262 } 263 264 assert(0); 265 } 266 }