#!/usr/bin/env dub /+dub.sdl: dependency "dub" path="../.." +/ import std.algorithm, std.conv, std.format, std.path, std.range; import std.stdio : File; import dub.internal.dyaml.stdsumtype; import dub.commandline; static struct Config { import std.datetime; SysTime date; string[] relatedSubCommands; static Config init(){ import std.process : environment; Config config; config.date = Clock.currTime; auto diffable = environment.get("DIFFABLE", "0"); if (diffable == "1") config.date = SysTime(DateTime(2018, 01, 01)); config.cwd = __FILE_FULL_PATH__.dirName; return config; } string cwd; } struct ManWriter { enum Mode { man, markdown } File output; Mode mode; string escapeWord(string s) { final switch (mode) { case Mode.man: return s.replace(`\`, `\\`).replace(`-`, `\-`).replace(`.`, `\&.`); case Mode.markdown: return s.replace(`<`, `<`).replace(`>`, `>`); } } string escapeFulltext(string s) { final switch (mode) { case Mode.man: return s; case Mode.markdown: return s.replace(`<`, `<`).replace(`>`, `>`); } } string italic(string w) { final switch (mode) { case Mode.man: return `\fI` ~ w ~ `\fR`; case Mode.markdown: return `<i>` ~ w ~ `</i>`; } } string bold(string w) { final switch (mode) { case Mode.man: return `\fB` ~ w ~ `\fR`; case Mode.markdown: return `<b>` ~ w ~ `</b>`; } } string header(string heading) { final switch (mode) { case Mode.man: return ".SH " ~ heading; case Mode.markdown: return "## " ~ heading; } } string subheader(string heading) { final switch (mode) { case Mode.man: return ".SS " ~ heading; case Mode.markdown: return "### " ~ heading; } } string url(string urlAndText) { return url(urlAndText, urlAndText); } string url(string url, string text) { final switch (mode) { case Mode.man: return ".UR" ~ url ~ "\n" ~ text ~ "\n.UE"; case Mode.markdown: return format!"[%s](%s)"(text, url); } } string autolink(string s) { final switch (mode) { case Mode.man: return s; case Mode.markdown: auto sanitized = s .replace("<b>", "") .replace("</b>", "") .replace("<i>", "") .replace("</i>", "") .replace("*", ""); if (sanitized.startsWith("dub") && sanitized.endsWith("(1)")) { sanitized = sanitized[0 .. $ - 3]; return url(sanitized ~ ".md", s); } return s; } } /// Links subcommands in the main dub.md file (converts the subcommand name /// like `init` into a link to `dub-init.md`) string specialLinkMainCmd(string s) { final switch (mode) { case Mode.man: return s; case Mode.markdown: return url("dub-" ~ s ~ ".md", s); } } void write(T...)(T args) { output.write(args); } void writeln(T...)(T args) { output.writeln(args); } void writefln(T...)(T args) { output.writefln(args); } void writeHeader(string manName, const Config config) { import std.uni : toLower; final switch (mode) { case Mode.man: static immutable manHeader = `.TH %s 1 "%s" "The D Language Foundation" "The D Language Foundation" .SH NAME`; writefln(manHeader, manName, config.date.toISOExtString.take(10)); break; case Mode.markdown: writefln("# %s(1)", manName.toLower); break; } } void writeFooter(string seeAlso, const Config config) { const manFooter = header("FILES") ~ '\n' ~ italic(escapeWord("dub.sdl")) ~ ", " ~ italic(escapeWord("dub.json")) ~ '\n' ~ header("AUTHOR") ~ '\n' ~ `Copyright (c) 1999-%s by The D Language Foundation` ~ '\n' ~ header("ONLINE DOCUMENTATION") ~ '\n' ~ url(`http://code.dlang.org/docs/commandline`) ~ '\n' ~ header("SEE ALSO"); writefln(manFooter, config.date.year); writeln(seeAlso); } string highlightArguments(string args) { import std.regex : regex, replaceAll; static auto re = regex("<([^>]*)>"); const reReplacement = escapeWord("<%s>").format(italic(escapeWord(`$1`))); auto ret = args.replaceAll(re, reReplacement); if (ret.length) ret ~= ' '; return ret; } void beginArgs(string cmd) { if (mode == Mode.markdown) writeln("\n<dl>\n"); } void endArgs() { if (mode == Mode.markdown) writeln("\n</dl>\n"); } void writeArgName(string cmd, string name) { import std.regex : regex, replaceAll; final switch ( mode ) { case Mode.man: writeln(".PP"); writeln(name); break; case Mode.markdown: string nameEscape = name.replaceAll(regex("[^a-zA-Z0-9_-]+"), "-"); writeln(); writefln(`<dt id="option-%s--%s" class="option-argname">`, cmd, nameEscape); writefln(`<a class="anchor" href="#option-%s--%s"></a>`, cmd, nameEscape); writeln(); writeln(name); writeln(); writeln(`</dt>`); writeln(); break; } } void beginArgDescription() { final switch ( mode ) { case Mode.man: writeln(".RS 4"); break; case Mode.markdown: writeln(); writefln(`<dd markdown="1" class="option-desc">`); writeln(); break; } } void endArgDescription() { final switch ( mode ) { case Mode.man: writeln(".RE"); break; case Mode.markdown: writeln(); writefln(`</dd>`); writeln(); break; } } void writeArgs(string cmdName, CommandArgs args) { beginArgs(cmdName); foreach (arg; args.recognizedArgs) { auto names = arg.names.split("|"); assert(names.length == 1 || names.length == 2); string sarg = names[0].length == 1 ? names[0] : null; string larg = names[0].length > 1 ? names[0] : names.length > 1 ? names[1] : null; string name; if (sarg !is null) { name ~= bold(escapeWord("-%s".format(sarg))); if (larg !is null) name ~= ", "; } if (larg !is null) { name ~= bold(escapeWord("--%s".format(larg))); if (arg.defaultValue.match!((bool b) => false, _ => true)) name ~= escapeWord("=") ~ italic("VALUE"); } writeArgName(cmdName, name); beginArgDescription(); writeln(arg.helpText.join(mode == Mode.man ? "\n" : "\n\n")); endArgDescription(); } endArgs(); } void writeDefinition(string key, string definition) { final switch (mode) { case Mode.man: writeln(".TP"); writeln(bold(key)); writeln(definition); break; case Mode.markdown: writeln(`<dt markdown="1">`); writeln(); writeln(bold(key)); writeln(); writeln("</dt>"); writeln(`<dd markdown="1">`); writeln(); writeln(definition); writeln(); writeln("</dd>"); break; } } void beginDefinitionList() { final switch (mode) { case Mode.man: break; case Mode.markdown: writeln(); writeln(`<dl markdown="1">`); writeln(); break; } } void endDefinitionList() { final switch (mode) { case Mode.man: break; case Mode.markdown: writeln("\n</dl>\n"); break; } } void writeDefaultExitCodes() { string[2][] exitCodes = [ ["0", "DUB succeeded"], ["1", "usage errors, unknown command line flags"], ["2", "package not found, package failed to load, miscellaneous error"] ]; final switch (mode) { case Mode.man: foreach (cm; exitCodes) { writeln(".TP"); writeln(".BR ", cm[0]); writeln(cm[1]); } break; case Mode.markdown: beginDefinitionList(); foreach (cm; exitCodes) { writeDefinition(cm[0], cm[1]); } endDefinitionList(); break; } } } void writeMainManFile(CommandArgs args, CommandGroup[] commands, string fileName, const Config config) { auto manFile = ManWriter( File(config.cwd.buildPath(fileName), "w"), fileName.endsWith(".md") ? ManWriter.Mode.markdown : ManWriter.Mode.man ); manFile.writeHeader("DUB", config); auto seeAlso = [ manFile.autolink(manFile.bold("dmd") ~ "(1)"), manFile.autolink(manFile.bold("rdmd") ~ "(1)") ] .chain(commands .map!(a => a.commands) .joiner .map!(cmd => manFile.autolink(manFile.bold("dub-" ~ cmd.name) ~ "(1)"))) .joiner(", ") .to!string; scope(exit) manFile.writeFooter(seeAlso, config); alias writeln = (m) => manFile.writeln(m); writeln(`dub \- Package and build management system for D`); writeln(manFile.header("SYNOPSIS")); writeln(manFile.bold("dub") ~ text( " [", manFile.escapeWord("--version"), "] [", manFile.italic("COMMAND"), "] [", manFile.italic(manFile.escapeWord("OPTIONS...")), "] ", manFile.escapeWord("--"), " [", manFile.italic(manFile.escapeWord("APPLICATION ARGUMENTS...")), "]" )); writeln(manFile.header("DESCRIPTION")); writeln(`Manages the DUB project in the current directory. DUB can serve as a build system and a package manager, automatically keeping track of project's dependencies \- both downloading them and linking them into the application.`); writeln(manFile.header("COMMANDS")); manFile.beginDefinitionList(); foreach (grp; commands) { foreach (cmd; grp.commands) { manFile.writeDefinition(manFile.specialLinkMainCmd(cmd.name), cmd.helpText.join( manFile.mode == ManWriter.Mode.markdown ? "\n\n" : "\n" )); } } writeln(manFile.header("COMMON OPTIONS")); manFile.writeArgs("-", args); } void writeManFile(Command command, const Config config, ManWriter.Mode mode) { import std.uni : toUpper; auto args = new CommandArgs(null); command.prepare(args); string fileName = format(mode == ManWriter.Mode.markdown ? "dub-%s.md" : "dub-%s.1", command.name); auto manFile = ManWriter(File(config.cwd.buildPath(fileName), "w"), mode); auto manName = format("DUB-%s", command.name).toUpper; manFile.writeHeader(manName, config); string[] extraRelated; foreach (arg; args.recognizedArgs) { if (arg.names.canFind("rdmd")) extraRelated ~= manFile.autolink(manFile.bold("rdmd") ~ "(1)"); } if (command.name == "dustmite") extraRelated ~= manFile.autolink(manFile.bold("dustmite") ~ "(1)"); const seeAlso = [manFile.autolink(manFile.bold("dub") ~ "(1)")] .chain(config.relatedSubCommands.map!(s => manFile.autolink(manFile.bold("dub-" ~ s) ~ "(1)"))) .chain(extraRelated) .joiner(", ") .to!string; scope(exit) manFile.writeFooter(seeAlso, config); alias writeln = (m) => manFile.writeln(m); manFile.writefln(`dub-%s \- %s`, command.name, manFile.escapeFulltext(command.description)); writeln(manFile.header("SYNOPSIS")); manFile.write(manFile.bold("dub %s ".format(command.name))); manFile.write(manFile.highlightArguments(command.argumentsPattern)); writeln(manFile.italic(manFile.escapeWord(`OPTIONS...`))); if (command.acceptsAppArgs) { writeln("[-- <%s>]".format(manFile.italic(manFile.escapeWord("application arguments...")))); } writeln(manFile.header("DESCRIPTION")); writeln(manFile.escapeFulltext(command.helpText.join("\n\n"))); writeln(manFile.header("OPTIONS")); manFile.writeArgs(command.name, args); writeln(manFile.subheader("COMMON OPTIONS")); manFile.writeln("See ", manFile.autolink(manFile.bold("dub") ~ "(1)")); manFile.writeln(manFile.header("EXIT STATUS")); if (command.name == "dustmite") { manFile.writeln("Forwards the exit code from " ~ manFile.autolink(manFile.bold(`dustmite`) ~ `(1)`)); } else { manFile.writeDefaultExitCodes(); } } void main() { Config config = Config.init; auto commands = getCommands(); // main dub.1 { CommonOptions options; auto args = new CommandArgs(null); options.prepare(args); args.writeMainManFile(commands, "dub.1", config); args.writeMainManFile(commands, "dub.md", config); } string[][] relatedSubCommands = [ ["run", "build", "test"], ["test", "dustmite", "lint"], ["describe", "generate"], ["add", "fetch"], ["init", "add", "convert"], ["add-path", "remove-path"], ["add-local", "remove-local"], ["list", "search"], ["add-override", "remove-override", "list-overrides"], ["clean-caches", "clean", "remove"], ]; // options for each specific command foreach (cmd; commands.map!(a => a.commands).joiner) { string[] related; foreach (relatedList; relatedSubCommands) { if (relatedList.canFind(cmd.name)) related ~= relatedList; } related = related.sort!"a<b".uniq.array; related = related.remove!(c => c == cmd.name); config.relatedSubCommands = related; cmd.writeManFile(config, ManWriter.Mode.man); cmd.writeManFile(config, ManWriter.Mode.markdown); } }