Newer
Older
dub_jkp / scripts / man / gen_man.d
  1. #!/usr/bin/env dub
  2. /+dub.sdl:
  3. dependency "dub" path="../.."
  4. +/
  5.  
  6. import std.algorithm, std.conv, std.format, std.path, std.range;
  7. import std.stdio : File;
  8. import dub.internal.dyaml.stdsumtype;
  9. import dub.commandline;
  10.  
  11. static struct Config
  12. {
  13. import std.datetime;
  14. SysTime date;
  15. string[] relatedSubCommands;
  16.  
  17. static Config init(){
  18. import std.process : environment;
  19. Config config;
  20. config.date = Clock.currTime;
  21. auto diffable = environment.get("DIFFABLE", "0");
  22. if (diffable == "1")
  23. config.date = SysTime(DateTime(2018, 01, 01));
  24.  
  25. config.cwd = __FILE_FULL_PATH__.dirName;
  26. return config;
  27. }
  28. string cwd;
  29. }
  30.  
  31. struct ManWriter
  32. {
  33. enum Mode
  34. {
  35. man, markdown
  36. }
  37.  
  38. File output;
  39. Mode mode;
  40.  
  41. string escapeWord(string s)
  42. {
  43. final switch (mode) {
  44. case Mode.man: return s.replace(`\`, `\\`).replace(`-`, `\-`).replace(`.`, `\&.`);
  45. case Mode.markdown: return s.replace(`<`, `&lt;`).replace(`>`, `&gt;`);
  46. }
  47. }
  48.  
  49. string escapeFulltext(string s)
  50. {
  51. final switch (mode) {
  52. case Mode.man: return s;
  53. case Mode.markdown: return s.replace(`<`, `&lt;`).replace(`>`, `&gt;`);
  54. }
  55. }
  56.  
  57. string italic(string w)
  58. {
  59. final switch (mode) {
  60. case Mode.man: return `\fI` ~ w ~ `\fR`;
  61. case Mode.markdown: return `<i>` ~ w ~ `</i>`;
  62. }
  63. }
  64.  
  65. string bold(string w)
  66. {
  67. final switch (mode) {
  68. case Mode.man: return `\fB` ~ w ~ `\fR`;
  69. case Mode.markdown: return `<b>` ~ w ~ `</b>`;
  70. }
  71. }
  72.  
  73. string header(string heading)
  74. {
  75. final switch (mode) {
  76. case Mode.man: return ".SH " ~ heading;
  77. case Mode.markdown: return "## " ~ heading;
  78. }
  79. }
  80.  
  81. string subheader(string heading)
  82. {
  83. final switch (mode) {
  84. case Mode.man: return ".SS " ~ heading;
  85. case Mode.markdown: return "### " ~ heading;
  86. }
  87. }
  88.  
  89. string url(string urlAndText)
  90. {
  91. return url(urlAndText, urlAndText);
  92. }
  93.  
  94. string url(string url, string text)
  95. {
  96. final switch (mode) {
  97. case Mode.man: return ".UR" ~ url ~ "\n" ~ text ~ "\n.UE";
  98. case Mode.markdown: return format!"[%s](%s)"(text, url);
  99. }
  100. }
  101.  
  102. string autolink(string s)
  103. {
  104. final switch (mode) {
  105. case Mode.man: return s;
  106. case Mode.markdown:
  107. auto sanitized = s
  108. .replace("<b>", "")
  109. .replace("</b>", "")
  110. .replace("<i>", "")
  111. .replace("</i>", "")
  112. .replace("*", "");
  113. if (sanitized.startsWith("dub") && sanitized.endsWith("(1)")) {
  114. sanitized = sanitized[0 .. $ - 3];
  115. return url(sanitized ~ ".md", s);
  116. }
  117. return s;
  118. }
  119. }
  120.  
  121. /// Links subcommands in the main dub.md file (converts the subcommand name
  122. /// like `init` into a link to `dub-init.md`)
  123. string specialLinkMainCmd(string s)
  124. {
  125. final switch (mode) {
  126. case Mode.man: return s;
  127. case Mode.markdown: return url("dub-" ~ s ~ ".md", s);
  128. }
  129. }
  130.  
  131. void write(T...)(T args)
  132. {
  133. output.write(args);
  134. }
  135.  
  136. void writeln(T...)(T args)
  137. {
  138. output.writeln(args);
  139. }
  140.  
  141. void writefln(T...)(T args)
  142. {
  143. output.writefln(args);
  144. }
  145.  
  146. void writeHeader(string manName, const Config config)
  147. {
  148. import std.uni : toLower;
  149.  
  150. final switch (mode)
  151. {
  152. case Mode.man:
  153. static immutable manHeader =
  154. `.TH %s 1 "%s" "The D Language Foundation" "The D Language Foundation"
  155. .SH NAME`;
  156. writefln(manHeader, manName, config.date.toISOExtString.take(10));
  157. break;
  158. case Mode.markdown:
  159. writefln("# %s(1)", manName.toLower);
  160. break;
  161. }
  162. }
  163.  
  164. void writeFooter(string seeAlso, const Config config)
  165. {
  166. const manFooter =
  167. header("FILES") ~ '\n'
  168. ~ italic(escapeWord("dub.sdl")) ~ ", " ~ italic(escapeWord("dub.json")) ~ '\n'
  169. ~ header("AUTHOR") ~ '\n'
  170. ~ `Copyright (c) 1999-%s by The D Language Foundation` ~ '\n'
  171. ~ header("ONLINE DOCUMENTATION") ~ '\n'
  172. ~ url(`http://code.dlang.org/docs/commandline`) ~ '\n'
  173. ~ header("SEE ALSO");
  174. writefln(manFooter, config.date.year);
  175. writeln(seeAlso);
  176. }
  177.  
  178. string highlightArguments(string args)
  179. {
  180. import std.regex : regex, replaceAll;
  181. static auto re = regex("<([^>]*)>");
  182. const reReplacement = escapeWord("<%s>").format(italic(escapeWord(`$1`)));
  183. auto ret = args.replaceAll(re, reReplacement);
  184. if (ret.length) ret ~= ' ';
  185. return ret;
  186. }
  187.  
  188. void beginArgs(string cmd)
  189. {
  190. if (mode == Mode.markdown)
  191. writeln("\n<dl>\n");
  192. }
  193.  
  194. void endArgs()
  195. {
  196. if (mode == Mode.markdown)
  197. writeln("\n</dl>\n");
  198. }
  199.  
  200. void writeArgName(string cmd, string name)
  201. {
  202. import std.regex : regex, replaceAll;
  203. final switch ( mode )
  204. {
  205. case Mode.man:
  206. writeln(".PP");
  207. writeln(name);
  208. break;
  209. case Mode.markdown:
  210. string nameEscape = name.replaceAll(regex("[^a-zA-Z0-9_-]+"), "-");
  211. writeln();
  212. writefln(`<dt id="option-%s--%s" class="option-argname">`, cmd, nameEscape);
  213. writefln(`<a class="anchor" href="#option-%s--%s"></a>`, cmd, nameEscape);
  214. writeln();
  215. writeln(name);
  216. writeln();
  217. writeln(`</dt>`);
  218. writeln();
  219. break;
  220. }
  221. }
  222.  
  223. void beginArgDescription()
  224. {
  225. final switch ( mode )
  226. {
  227. case Mode.man:
  228. writeln(".RS 4");
  229. break;
  230. case Mode.markdown:
  231. writeln();
  232. writefln(`<dd markdown="1" class="option-desc">`);
  233. writeln();
  234. break;
  235. }
  236. }
  237.  
  238. void endArgDescription()
  239. {
  240. final switch ( mode )
  241. {
  242. case Mode.man:
  243. writeln(".RE");
  244. break;
  245. case Mode.markdown:
  246. writeln();
  247. writefln(`</dd>`);
  248. writeln();
  249. break;
  250. }
  251. }
  252.  
  253. void writeArgs(string cmdName, CommandArgs args)
  254. {
  255. beginArgs(cmdName);
  256. foreach (arg; args.recognizedArgs)
  257. {
  258. auto names = arg.names.split("|");
  259. assert(names.length == 1 || names.length == 2);
  260. string sarg = names[0].length == 1 ? names[0] : null;
  261. string larg = names[0].length > 1 ? names[0] : names.length > 1 ? names[1] : null;
  262. string name;
  263. if (sarg !is null) {
  264. name ~= bold(escapeWord("-%s".format(sarg)));
  265. if (larg !is null)
  266. name ~= ", ";
  267. }
  268. if (larg !is null) {
  269. name ~= bold(escapeWord("--%s".format(larg)));
  270. if (arg.defaultValue.match!((bool b) => false, _ => true))
  271. name ~= escapeWord("=") ~ italic("VALUE");
  272. }
  273. writeArgName(cmdName, name);
  274. beginArgDescription();
  275. writeln(arg.helpText.join(mode == Mode.man ? "\n" : "\n\n"));
  276. endArgDescription();
  277. }
  278. endArgs();
  279. }
  280.  
  281. void writeDefinition(string key, string definition)
  282. {
  283. final switch (mode)
  284. {
  285. case Mode.man:
  286. writeln(".TP");
  287. writeln(bold(key));
  288. writeln(definition);
  289. break;
  290. case Mode.markdown:
  291. writeln(`<dt markdown="1">`);
  292. writeln();
  293. writeln(bold(key));
  294. writeln();
  295. writeln("</dt>");
  296. writeln(`<dd markdown="1">`);
  297. writeln();
  298. writeln(definition);
  299. writeln();
  300. writeln("</dd>");
  301. break;
  302. }
  303. }
  304.  
  305. void beginDefinitionList()
  306. {
  307. final switch (mode)
  308. {
  309. case Mode.man:
  310. break;
  311. case Mode.markdown:
  312. writeln();
  313. writeln(`<dl markdown="1">`);
  314. writeln();
  315. break;
  316. }
  317. }
  318.  
  319. void endDefinitionList()
  320. {
  321. final switch (mode)
  322. {
  323. case Mode.man:
  324. break;
  325. case Mode.markdown:
  326. writeln("\n</dl>\n");
  327. break;
  328. }
  329. }
  330.  
  331. void writeDefaultExitCodes()
  332. {
  333. string[2][] exitCodes = [
  334. ["0", "DUB succeeded"],
  335. ["1", "usage errors, unknown command line flags"],
  336. ["2", "package not found, package failed to load, miscellaneous error"]
  337. ];
  338.  
  339. final switch (mode)
  340. {
  341. case Mode.man:
  342. foreach (cm; exitCodes) {
  343. writeln(".TP");
  344. writeln(".BR ", cm[0]);
  345. writeln(cm[1]);
  346. }
  347. break;
  348. case Mode.markdown:
  349. beginDefinitionList();
  350. foreach (cm; exitCodes) {
  351. writeDefinition(cm[0], cm[1]);
  352. }
  353. endDefinitionList();
  354. break;
  355. }
  356. }
  357. }
  358.  
  359. void writeMainManFile(CommandArgs args, CommandGroup[] commands,
  360. string fileName, const Config config)
  361. {
  362. auto manFile = ManWriter(
  363. File(config.cwd.buildPath(fileName), "w"),
  364. fileName.endsWith(".md") ? ManWriter.Mode.markdown : ManWriter.Mode.man
  365. );
  366. manFile.writeHeader("DUB", config);
  367. auto seeAlso = [
  368. manFile.autolink(manFile.bold("dmd") ~ "(1)"),
  369. manFile.autolink(manFile.bold("rdmd") ~ "(1)")
  370. ]
  371. .chain(commands
  372. .map!(a => a.commands)
  373. .joiner
  374. .map!(cmd => manFile.autolink(manFile.bold("dub-" ~ cmd.name) ~ "(1)")))
  375. .joiner(", ")
  376. .to!string;
  377. scope(exit) manFile.writeFooter(seeAlso, config);
  378.  
  379. alias writeln = (m) => manFile.writeln(m);
  380. writeln(`dub \- Package and build management system for D`);
  381. writeln(manFile.header("SYNOPSIS"));
  382. writeln(manFile.bold("dub") ~ text(
  383. " [",
  384. manFile.escapeWord("--version"),
  385. "] [",
  386. manFile.italic("COMMAND"),
  387. "] [",
  388. manFile.italic(manFile.escapeWord("OPTIONS...")),
  389. "] ", manFile.escapeWord("--"), " [",
  390. manFile.italic(manFile.escapeWord("APPLICATION ARGUMENTS...")),
  391. "]"
  392. ));
  393.  
  394. writeln(manFile.header("DESCRIPTION"));
  395. writeln(`Manages the DUB project in the current directory. DUB can serve as a build
  396. system and a package manager, automatically keeping track of project's
  397. dependencies \- both downloading them and linking them into the application.`);
  398.  
  399. writeln(manFile.header("COMMANDS"));
  400. manFile.beginDefinitionList();
  401. foreach (grp; commands) {
  402. foreach (cmd; grp.commands) {
  403. manFile.writeDefinition(manFile.specialLinkMainCmd(cmd.name), cmd.helpText.join(
  404. manFile.mode == ManWriter.Mode.markdown ? "\n\n" : "\n"
  405. ));
  406. }
  407. }
  408.  
  409.  
  410.  
  411. writeln(manFile.header("COMMON OPTIONS"));
  412. manFile.writeArgs("-", args);
  413. }
  414.  
  415. void writeManFile(Command command, const Config config, ManWriter.Mode mode)
  416. {
  417. import std.uni : toUpper;
  418.  
  419. auto args = new CommandArgs(null);
  420. command.prepare(args);
  421. string fileName = format(mode == ManWriter.Mode.markdown ? "dub-%s.md" : "dub-%s.1", command.name);
  422. auto manFile = ManWriter(File(config.cwd.buildPath(fileName), "w"), mode);
  423. auto manName = format("DUB-%s", command.name).toUpper;
  424. manFile.writeHeader(manName, config);
  425.  
  426. string[] extraRelated;
  427. foreach (arg; args.recognizedArgs) {
  428. if (arg.names.canFind("rdmd"))
  429. extraRelated ~= manFile.autolink(manFile.bold("rdmd") ~ "(1)");
  430. }
  431. if (command.name == "dustmite")
  432. extraRelated ~= manFile.autolink(manFile.bold("dustmite") ~ "(1)");
  433.  
  434. const seeAlso = [manFile.autolink(manFile.bold("dub") ~ "(1)")]
  435. .chain(config.relatedSubCommands.map!(s => manFile.autolink(manFile.bold("dub-" ~ s) ~ "(1)")))
  436. .chain(extraRelated)
  437. .joiner(", ")
  438. .to!string;
  439. scope(exit) manFile.writeFooter(seeAlso, config);
  440.  
  441. alias writeln = (m) => manFile.writeln(m);
  442.  
  443. manFile.writefln(`dub-%s \- %s`, command.name, manFile.escapeFulltext(command.description));
  444.  
  445. writeln(manFile.header("SYNOPSIS"));
  446. manFile.write(manFile.bold("dub %s ".format(command.name)));
  447. manFile.write(manFile.highlightArguments(command.argumentsPattern));
  448. writeln(manFile.italic(manFile.escapeWord(`OPTIONS...`)));
  449. if (command.acceptsAppArgs)
  450. {
  451. writeln("[-- <%s>]".format(manFile.italic(manFile.escapeWord("application arguments..."))));
  452. }
  453.  
  454. writeln(manFile.header("DESCRIPTION"));
  455. writeln(manFile.escapeFulltext(command.helpText.join("\n\n")));
  456. writeln(manFile.header("OPTIONS"));
  457. manFile.writeArgs(command.name, args);
  458.  
  459. writeln(manFile.subheader("COMMON OPTIONS"));
  460. manFile.writeln("See ", manFile.autolink(manFile.bold("dub") ~ "(1)"));
  461.  
  462. manFile.writeln(manFile.header("EXIT STATUS"));
  463. if (command.name == "dustmite") {
  464. manFile.writeln("Forwards the exit code from " ~ manFile.autolink(manFile.bold(`dustmite`) ~ `(1)`));
  465. } else {
  466. manFile.writeDefaultExitCodes();
  467. }
  468. }
  469.  
  470. void main()
  471. {
  472. Config config = Config.init;
  473. auto commands = getCommands();
  474.  
  475. // main dub.1
  476. {
  477. CommonOptions options;
  478. auto args = new CommandArgs(null);
  479. options.prepare(args);
  480. args.writeMainManFile(commands, "dub.1", config);
  481. args.writeMainManFile(commands, "dub.md", config);
  482. }
  483.  
  484. string[][] relatedSubCommands = [
  485. ["run", "build", "test"],
  486. ["test", "dustmite", "lint"],
  487. ["describe", "generate"],
  488. ["add", "fetch"],
  489. ["init", "add", "convert"],
  490. ["add-path", "remove-path"],
  491. ["add-local", "remove-local"],
  492. ["list", "search"],
  493. ["add-override", "remove-override", "list-overrides"],
  494. ["clean-caches", "clean", "remove"],
  495. ];
  496.  
  497. // options for each specific command
  498. foreach (cmd; commands.map!(a => a.commands).joiner) {
  499. string[] related;
  500. foreach (relatedList; relatedSubCommands) {
  501. if (relatedList.canFind(cmd.name))
  502. related ~= relatedList;
  503. }
  504. related = related.sort!"a<b".uniq.array;
  505. related = related.remove!(c => c == cmd.name);
  506. config.relatedSubCommands = related;
  507.  
  508. cmd.writeManFile(config, ManWriter.Mode.man);
  509. cmd.writeManFile(config, ManWriter.Mode.markdown);
  510. }
  511. }