1 /++ 2 $(H2 The Help Type for Cmdline) 3 4 This modlue mainly has `Help` Type. 5 We can change it to control the behaviors 6 of the cmd-line program's help command and help option 7 8 Authors: 笑愚(xiaoyu) 9 +/ 10 module cmdline.help; 11 12 import std.algorithm; 13 import std.array; 14 import std.string : stripRight; 15 import std.regex; 16 import std.format; 17 import std.typecons; 18 import std.uni; 19 import std.range; 20 21 import cmdline.pattern; 22 import cmdline.option; 23 import cmdline.argument; 24 import cmdline.command; 25 26 class Help { 27 /// the help print's max coloum width 28 int helpWidth = 80; 29 /// whether sort the sub commands on help print 30 bool sortSubCommands = false; 31 /// whether sort the options on help print 32 bool sortOptions = false; 33 /// whether show global options, which is not recommended to turn on it 34 bool showGlobalOptions = false; 35 36 /// get the list of not hidden sub commands 37 inout(Command)[] visibleCommands(inout(Command) cmd) const { 38 Command cmd_tmp = cast(Command) cmd; 39 Command[] visible_cmds = cmd_tmp._commands.filter!(c => !c._hidden).array; 40 auto help_cmd = cmd_tmp._getHelpCommand(); 41 auto version_cmd = cmd_tmp._versionCommand; 42 if (help_cmd && !help_cmd._hidden) { 43 visible_cmds ~= help_cmd; 44 } 45 if (version_cmd && !version_cmd._hidden) { 46 visible_cmds ~= version_cmd; 47 } 48 if (this.sortSubCommands) { 49 return cast(inout(Command[])) visible_cmds.sort!((a, b) => a.name < b.name).array; 50 } 51 return cast(inout(Command[])) visible_cmds; 52 } 53 54 /// get the list of not hidden options 55 inout(Option)[] visibleOptions(inout(Command) command) const { 56 auto cmd = cast(Command) command; 57 Option[] visible_opts = (cmd._options).filter!(opt => !opt.hidden).array; 58 visible_opts ~= cmd._abandons.filter!(opt => !opt.hidden).array; 59 auto help_opt = cmd._getHelpOption(); 60 auto version_opt = cmd._versionOption; 61 auto config_opt = cmd._configOption; 62 if (help_opt && !help_opt.hidden) 63 visible_opts ~= help_opt; 64 if (version_opt && !version_opt.hidden) 65 visible_opts ~= version_opt; 66 if (config_opt && !config_opt.hidden) 67 visible_opts ~= config_opt; 68 if (this.sortOptions) { 69 return cast(inout(Option[])) visible_opts.sort!((a, b) => a.name < b.name).array; 70 } 71 return cast(inout(Option[])) visible_opts; 72 } 73 74 /// get the list of not hidden negate options 75 inout(NegateOption)[] visibleNegateOptions(inout(Command) command) const { 76 auto cmd = cast(Command) command; 77 auto visible_opts = (cmd._negates).filter!(opt => !opt.hidden).array; 78 if (this.sortOptions) { 79 return cast(inout(NegateOption[])) visible_opts.sort!((a, b) => a.name < b.name).array; 80 } 81 return cast(inout(NegateOption[])) visible_opts; 82 } 83 84 /// get the list of not hidden global options 85 inout(Option)[] visibleGlobalOptions(inout(Command) command) const { 86 if (!this.showGlobalOptions) 87 return []; 88 auto cmds_global = command._getCommandAndAncestors(); 89 auto ancestor_cmd = (cast(Command[]) cmds_global)[1 .. $]; 90 Option[] visible_opts = []; 91 foreach (cmd; ancestor_cmd) { 92 visible_opts ~= (cmd._options).filter!(opt => !opt.hidden).array; 93 visible_opts ~= cmd._abandons.filter!(opt => !opt.hidden).array; 94 } 95 if (this.sortOptions) { 96 return cast(inout(Option[])) visible_opts.sort!((a, b) => a.name < b.name).array; 97 } 98 return cast(inout(Option[])) visible_opts; 99 } 100 101 /// get the list of not hidden global negate options 102 inout(NegateOption)[] visibleGlobalNegateOptions(inout(Command) command) const { 103 if (!this.showGlobalOptions) 104 return []; 105 auto cmds_global = command._getCommandAndAncestors(); 106 auto ancestor_cmd = (cast(Command[]) cmds_global)[1 .. $]; 107 NegateOption[] visible_opts = []; 108 foreach (cmd; ancestor_cmd) 109 visible_opts ~= (cmd._negates).filter!(opt => !opt.hidden).array; 110 if (this.sortOptions) { 111 return cast(inout(NegateOption[])) visible_opts.sort!((a, b) => a.name < b.name).array; 112 } 113 return cast(inout(NegateOption[])) visible_opts; 114 } 115 116 /// get the list of arguments 117 inout(Argument)[] visibleArguments(inout(Command) command) const { 118 if (command._argsDescription) { 119 Command cmd = cast(Command) command; 120 cmd._arguments.each!((arg) { 121 if (arg.description == "") { 122 auto tmp = arg.name in cmd._argsDescription; 123 if (tmp) 124 arg.description = *tmp; 125 } 126 }); 127 } 128 return command._arguments; 129 } 130 131 package: 132 string subCommandTerm(in Command cmd) const { 133 auto args_str = ""; 134 if (cmd._arguments.length) 135 args_str = cmd._arguments.map!(arg => arg.readableArgName).join(" "); 136 return ( 137 cmd._name ~ 138 (cmd._aliasNames.empty ? "" : "|" ~ cmd._aliasNames[0]) ~ 139 (cmd._options.empty ? "" : " [options]") ~ 140 (args_str == "" ? args_str : " " ~ args_str) ~ 141 (cmd._execHandler ? " >> " ~ cmd._usage : "") 142 ); 143 } 144 145 string optionTerm(in Option opt) const { 146 return opt.flags; 147 } 148 149 string optionTerm(in NegateOption opt) const { 150 return opt.flags; 151 } 152 153 string argumentTerm(in Argument arg) const { 154 return arg.name; 155 } 156 157 int longestSubcommandTermLength(in Command cmd) const { 158 return reduce!((int mn, command) { 159 return max(mn, cast(int) subCommandTerm(command).length); 160 })(0, visibleCommands(cmd)); 161 } 162 163 int longestOptionTermLength(in Command cmd) const { 164 int opt_len = reduce!((int mn, opt) { 165 return max(mn, cast(int) optionTerm(opt).length); 166 })(0, visibleOptions(cmd)); 167 168 int nopt_len = reduce!((int mn, opt) { 169 return max(mn, cast(int) optionTerm(opt).length); 170 })(0, visibleNegateOptions(cmd)); 171 172 return max(opt_len, nopt_len); 173 } 174 175 int longestGlobalOptionTermLength(in Command cmd) const { 176 int opt_len = reduce!((int mn, opt) { 177 return max(mn, cast(int) optionTerm(opt).length); 178 })(0, visibleGlobalOptions(cmd)); 179 180 int nopt_len = reduce!((int mn, opt) { 181 return max(mn, cast(int) optionTerm(opt).length); 182 })(0, visibleGlobalNegateOptions(cmd)); 183 184 return max(opt_len, nopt_len); 185 } 186 187 int longestArgumentTermLength(in Command cmd) const { 188 return reduce!((int mn, arg) { 189 return max(mn, cast(int) argumentTerm(arg).length); 190 })(0, visibleArguments(cast(Command) cmd)); 191 } 192 193 /// get the usage of command 194 public string commandUsage(in Command cmd) const { 195 string cmd_name = cmd._name; 196 if (!cmd._aliasNames.empty) 197 cmd_name = cmd_name ~ "|" ~ cmd._aliasNames[0]; 198 string ancestor_name = ""; 199 auto tmp = cmd._getCommandAndAncestors(); 200 Command[] ancestor = (cast(Command[]) tmp)[1 .. $]; 201 ancestor.each!((c) { ancestor_name = c.name ~ " " ~ ancestor_name; }); 202 return ancestor_name ~ cmd_name ~ " " ~ cmd.usage; 203 } 204 205 string commandDesc(in Command cmd) const { 206 return cmd.description; 207 } 208 209 string optionDesc(in Option opt) const { 210 string[] info = [opt.description]; 211 auto type_str = opt.typeStr(); 212 auto choices_str = opt.choicesStr(); 213 auto default_str = opt.defaultValStr(); 214 auto preset_str = opt.presetStr(); 215 auto env_str = opt.envValStr(); 216 auto imply_str = opt.implyOptStr(); 217 auto conflict_str = opt.conflictOptStr(); 218 auto rangeof_str = opt.rangeOfStr(); 219 info ~= type_str; 220 info ~= default_str; 221 info ~= env_str; 222 info ~= preset_str; 223 info ~= rangeof_str; 224 info ~= choices_str; 225 info ~= imply_str; 226 info ~= conflict_str; 227 return info.filter!(str => !str.empty).join('\n'); 228 } 229 230 string optionDesc(in NegateOption opt) const { 231 return opt.description; 232 } 233 234 string argumentDesc(in Argument arg) const { 235 string[] info = [arg.description]; 236 auto type_str = arg.typeStr; 237 auto default_str = arg.defaultValStr; 238 auto rangeof_str = arg.rangeOfStr; 239 auto choices_str = arg.choicesStr; 240 info ~= type_str; 241 info ~= default_str; 242 info ~= rangeof_str; 243 info ~= choices_str; 244 return info.filter!(str => !str.empty).join('\n'); 245 } 246 247 string subCommandDesc(in Command cmd) const { 248 return cmd.description; 249 } 250 251 int paddWidth(in Command cmd) const { 252 auto lg_opl = longestOptionTermLength(cmd); 253 auto lg_gopl = longestGlobalOptionTermLength(cmd); 254 auto lg_scl = longestSubcommandTermLength(cmd); 255 auto lg_arl = longestArgumentTermLength(cmd); 256 return max( 257 lg_opl, 258 lg_gopl, 259 lg_scl, 260 lg_arl 261 ); 262 } 263 264 string formatHelp(in Command cmd) const { 265 auto term_width = paddWidth(cmd); 266 auto item_indent_width = 2; 267 auto item_sp_width = 2; 268 269 auto format_item = (string term, string desc) { 270 if (desc != "") { 271 string padded_term_str = term; 272 if (term_width + item_sp_width > term.length) { 273 auto pad_num = term_width + item_sp_width - term.length; 274 string space = ' '.repeat(pad_num).array; 275 padded_term_str = term ~ space; 276 } 277 auto full_text = padded_term_str ~ desc; 278 return this.wrap( 279 full_text, 280 this.helpWidth - item_indent_width, 281 term_width + item_sp_width); 282 } 283 return term; 284 }; 285 286 auto format_list = (string[] textArr) { 287 return textArr.join("\n").replaceAll(regex(`^`, "m"), " "); 288 }; 289 290 string[] output = ["Usage: " ~ this.commandUsage(cmd), ""]; 291 292 string cmd_desc = this.commandDesc(cmd); 293 if (cmd_desc.length > 0) { 294 output ~= [this.wrap(cmd_desc, this.helpWidth, 0), ""]; 295 } 296 297 if (!cmd._defaultCommandName.empty) { 298 string str = format!"default sub command is `%s`"(cmd._defaultCommandName); 299 output ~= [this.wrap(str, this.helpWidth, 0), ""]; 300 } 301 302 string[] arg_list = visibleArguments(cmd).map!( 303 arg => format_item(argumentTerm(arg), argumentDesc(arg))).array; 304 if (arg_list.length) { 305 output ~= ["Arguments:", format_list(arg_list), ""]; 306 } 307 308 string[] opt_list = visibleOptions(cmd).map!( 309 opt => format_item(optionTerm(opt), optionDesc(opt))).array; 310 string[] negate_list = visibleNegateOptions(cmd).map!( 311 opt => format_item(optionTerm(opt), optionDesc(opt))).array; 312 opt_list ~= negate_list; 313 if (opt_list.length) { 314 output ~= ["Options:", format_list(opt_list), ""]; 315 } 316 317 if (this.showGlobalOptions) { 318 string[] global_list = visibleGlobalOptions(cmd).map!( 319 opt => format_item(optionTerm(opt), optionDesc(opt))).array; 320 string[] nglobal_list = visibleGlobalNegateOptions(cmd).map!( 321 opt => format_item(optionTerm(opt), optionDesc(opt))).array; 322 global_list ~= nglobal_list; 323 if (global_list.length) { 324 output ~= ["Global Options:", format_list(global_list), ""]; 325 } 326 } 327 328 string[] cmd_list = visibleCommands(cmd).map!( 329 c => format_item(subCommandTerm(c), subCommandDesc(c))).array; 330 if (cmd_list.length) { 331 output ~= ["Commands:", format_list(cmd_list), ""]; 332 } 333 334 return output.join("\n"); 335 } 336 337 string wrap(string str, int width, int indent) const { 338 if (!matchFirst(str, PTN_MANUALINDENT).empty) 339 return str; 340 auto text = str[indent .. $].replaceAll(regex(`\s+(?=\n|$)`), ""); 341 auto leading_str = str[0 .. indent]; 342 int col_width = width - indent; 343 string indent_str = repeat(' ', indent).array; 344 string ex_indent_str = " "; 345 string[] col_texts = text.split('\n').filter!(s => s.length).array; 346 auto get_front = () => col_texts.length ? col_texts.front : ""; 347 string[] tmp; 348 string cur_txt = get_front(); 349 if (cur_txt.length) 350 col_texts.popFront; 351 int cur_max_width = col_width; 352 bool is_ex_ing = cur_txt.length > cur_max_width; 353 if (is_ex_ing) { 354 col_texts.insertInPlace(0, cur_txt[cur_max_width .. $]); 355 cur_txt = cur_txt[0 .. cur_max_width]; 356 } 357 tmp ~= cur_txt; 358 while ((cur_txt = get_front()).length) { 359 col_texts.popFront; 360 bool flag = is_ex_ing; 361 cur_max_width = is_ex_ing ? col_width - 2 : col_width; 362 is_ex_ing = cur_txt.length > cur_max_width; 363 if (is_ex_ing) { 364 col_texts.insertInPlace(0, cur_txt[cur_max_width .. $]); 365 cur_txt = cur_txt[0 .. cur_max_width]; 366 } 367 tmp ~= flag ? ex_indent_str ~ indent_str ~ cur_txt : indent_str ~ cur_txt; 368 } 369 return leading_str ~ tmp.join("\r\n"); 370 } 371 } 372 373 private: 374 375 enum int maxDistance = 3; 376 377 int _editDistance(string a, string b) { 378 assert(max(a.length, b.length) < 20); 379 int xy = cast(int) a.length - cast(int) b.length; 380 if (xy < -maxDistance || xy > maxDistance) { 381 return cast(int) max(a.length, b.length); 382 } 383 int[20][20] dp = (int[20][20]).init; 384 for (int i = 0; i <= a.length; i++) 385 dp[i][0] = i; 386 for (int i = 0; i <= b.length; i++) 387 dp[0][i] = i; 388 int cost = 0; 389 for (int i = 1; i <= a.length; i++) { 390 for (int j = 1; j <= b.length; j++) { 391 if (a[i - 1] == b[j - 1]) 392 cost = 0; 393 else 394 cost = 1; 395 dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost); 396 } 397 } 398 return dp[a.length][b.length]; 399 } 400 401 package: 402 403 string suggestSimilar(string word, in string[] _candidates) { 404 if (!_candidates || _candidates.length == 0) 405 return ""; 406 auto searching_opts = word[0 .. 2] == "--"; 407 string[] candidates; 408 if (searching_opts) { 409 word = word[2 .. $].idup; 410 candidates = _candidates.map!(candidate => candidate[2 .. $].idup).array; 411 } 412 else { 413 candidates = _candidates.dup; 414 } 415 string[] similar = []; 416 int best_dist = maxDistance; 417 double min_similarity = 0.4; 418 candidates.each!((string candidate) { 419 if (candidates.length <= 1) 420 return No.each; 421 double dist = _editDistance(word, candidate); 422 double len = cast(double) max(word.length, candidate.length); 423 double similarity = (len - dist) / len; 424 if (similarity > min_similarity) { 425 if (dist < best_dist) { 426 best_dist = cast(int) dist; 427 similar ~= candidate; 428 } 429 else if (dist == best_dist) { 430 similar ~= candidate; 431 } 432 } 433 return Yes.each; 434 }); 435 similar.sort!((a, b) => a.toLower < b.toLower); 436 if (searching_opts) { 437 similar.each!((ref str) { str = "--" ~ str; }); 438 } 439 if (similar.length > 1) 440 return format("\n(Did you mean one of %s?)", similar.join(", ")); 441 if (similar.length == 1) 442 return format("\n(Did you mean %s?)", similar[0]); 443 return ""; 444 } 445 446 unittest { 447 import std.stdio; 448 449 writeln("===:", suggestSimilar("--son", [ 450 "--sone", "--sans", "--sou", "--don" 451 ])); 452 assert(_editDistance("kitten", "sitting") == 3); 453 assert(_editDistance("rosettacode", "raisethysword") == 8); 454 assert(_editDistance("", "") == 0); 455 assert(_editDistance("kitten", "") == 6); 456 assert(_editDistance("", "sitting") == 7); 457 assert(_editDistance("kitten", "kitten") == 0); 458 assert(_editDistance("meow", "woof") == 3); 459 assert(_editDistance("woof", "meow") == 3); 460 }