summaryrefslogtreecommitdiff
path: root/src/genhelp.lua
diff options
context:
space:
mode:
authorTimo Teräs <timo.teras@iki.fi>2020-04-24 11:49:14 +0300
committerTimo Teräs <timo.teras@iki.fi>2020-05-06 13:05:19 +0300
commit5258b484bf13441d3d5c199e3c8d70cf2a75b3ca (patch)
tree6c979789a3b26eab2479224be5f6919e49841019 /src/genhelp.lua
parentd61c009f7a7ba9c99797855d8cd4e0a503c2fda5 (diff)
downloadapk-tools-5258b484bf13441d3d5c199e3c8d70cf2a75b3ca.tar.gz
apk-tools-5258b484bf13441d3d5c199e3c8d70cf2a75b3ca.tar.bz2
apk-tools-5258b484bf13441d3d5c199e3c8d70cf2a75b3ca.tar.xz
apk-tools-5258b484bf13441d3d5c199e3c8d70cf2a75b3ca.zip
add script to autogenerate help from man pages
This creates main help like: -- usage: apk [<OPTIONS>...] COMMAND [<ARGUMENTS>...] Package installation and removal: add Add packages to WORLD and commit changes del Remove packages from WORLD and commit changes System maintenance: fix Check WORLD against the system and ensure consistency update Update repository indexes upgrade Install upgrades available from repositories cache Commands related to the management of an offline package cache Querying package information: info Give detailed information about packages or repositories list List packages matching a pattern or other criteria dot Generate graphviz graphs policy Show repository policy for packages Repository maintenance: index Create repository index file from packages fetch Download packages from global repositories to a local directory manifest Show checksums of package contents verify Verify package integrity and signature Miscellaneous: audit Audit directories for changes stats Show statistics about repositories and installations version Compare package versions or perform tests on version strings This apk has coffee making abilities. -- And applet specific help like: -- usage: apk add [<OPTIONS>...] PACKAGES... Description: apk add adds the requested packages to WORLD and installs (or upgrades) them if not already present, ensuring all dependencies are met. Options: --initdb Initialize a new package database -l, --latest Disables normal heuristics for choosing which repository to install a -u, --upgrade When adding packages which are already installed, upgrade them rather -t, --virtual NAME Instead of adding the specified packages to WORLD, create a new --no-chown Do not change file owner or group --
Diffstat (limited to 'src/genhelp.lua')
-rw-r--r--src/genhelp.lua300
1 files changed, 300 insertions, 0 deletions
diff --git a/src/genhelp.lua b/src/genhelp.lua
new file mode 100644
index 0000000..06a3590
--- /dev/null
+++ b/src/genhelp.lua
@@ -0,0 +1,300 @@
+#!/usr/bin/lua5.3
+
+--[[
+Utility to convert SCDOC manpages to apk-tools help messages
+
+General:
+ - Wrangle *apk-applet*(SECTION) links
+ - Uppercase _underlined_ things as they are "keywords"
+ - Other format specs like ** to be removed
+ - For options text, the first sentence (within the first line) is taken as the help text
+
+Main page: apk.8.scd
+ - SYNOPSIS
+ - COMMANDS has ## header with a table for commands list
+ - GLOBAL OPTIONS and COMMIT OPTIONS for option group help
+ - NOTES
+
+Applet pages: apk-*.8.scd
+ - Take usage from SYNOPSIS, can have multiple lines like apk-version(8)
+ - Take DESCRIPTION, take first paragraph, rewrap, and put as section in applet specific help
+ - From OPTIONS take each option and it's first sentence (within the first line)
+--]]
+
+local function splittokens(s)
+ local res = {}
+ for w in s:gmatch("%S+") do
+ res[#res+1] = w
+ end
+ return res
+end
+
+local function textwrap(text, linewidth)
+ local spaceleft = linewidth
+ local res = {}
+ local line = {}
+
+ for _, word in ipairs(splittokens(text)) do
+ if #word + 1 > spaceleft then
+ table.insert(res, table.concat(line, ' '))
+ line = { word }
+ spaceleft = linewidth - #word
+ else
+ table.insert(line, word)
+ spaceleft = spaceleft - (#word + 1)
+ end
+ end
+ table.insert(res, table.concat(line, ' '))
+ return res
+end
+
+local function upperfirst(s)
+ return s:sub(1,1):upper() .. s:sub(2):lower()
+end
+
+scdoc = {
+ usage_prefix = "usage: ",
+}
+scdoc.__index = scdoc
+
+function scdoc:nop(ln)
+ --print(self.section, ln)
+end
+
+function scdoc:SYNOPSIS_text(ln)
+ table.insert(self.usage, self.usage_prefix .. ln)
+ self.usage_prefix = " or: "
+end
+
+function scdoc:COMMANDS_text(ln)
+ local ch = ln:sub(1,1)
+ local a, b = ln:match("^([[|:<]*)%s+(.+)")
+ if ch == '|' then
+ self.cur_cmd = { b, "" }
+ table.insert(self.commands, self.cur_cmd)
+ elseif ch == ':' and self.cur_cmd then
+ self.cur_cmd[2] = b
+ self.cur_cmd = nil
+ end
+end
+
+function scdoc:COMMANDS_subsection(n)
+ n = n:sub(1,1) .. n:sub(2):lower()
+ table.insert(self.commands, n)
+end
+
+function scdoc:DESCRIPTION_text(ln)
+ table.insert(self.description, ln)
+end
+
+function scdoc:DESCRIPTION_paragraph()
+ if #self.description > 0 then
+ self.section_text = self.nop
+ end
+end
+
+function scdoc:OPTIONS_text(ln)
+ local ch = ln:sub(1,1)
+ if ch == '-' then
+ self.cur_opt = { ln, {} }
+ table.insert(self.options, self.cur_opt)
+ elseif ch == '\t' then
+ table.insert(self.cur_opt[2], ln:sub(2))
+ end
+end
+
+function scdoc:NOTES_text(ln)
+ table.insert(self.notes, ln)
+end
+
+function scdoc:parse_default(ln)
+ if #ln == 0 then
+ return (self[self.section .. "_paragraph"] or self.nop)(self)
+ end
+
+ s, n = ln:match("^(#*) (.*)")
+ if s and n then
+ if #s == 1 then
+ local optgroup, opts = n:match("^(%S*) ?(OPTIONS)$")
+ if opts then
+ if #optgroup == 0 then optgroup = self.applet end
+ self.options = { name = optgroup }
+ table.insert(self.optgroup, self.options)
+ n = opts
+ end
+
+ self.section = n
+ self.section_text = self[n .. "_text"] or self.nop
+ self.subsection = nil
+ else
+ self.subsection = n
+ local f = self[self.section.."_subsection"]
+ if f then f(self, n) end
+ end
+ return
+ end
+
+ -- Handle formatting
+ ln = ln:gsub("apk%-(%S+)%(%d%)", "%1")
+ ln = ln:gsub("([^\\])%*(.-[^\\])%*", "%1%2")
+ ln = ln:gsub("^%*(.-[^\\])%*", "%1")
+ ln = ln:gsub("([^\\])_(.-[^\\])_", function(a,s) return a..s:upper() end)
+ ln = ln:gsub("^_(.-[^\\])_", function(a,s) return a..s:upper() end)
+ ln = ln:gsub("\\", "")
+
+ self:section_text(ln)
+end
+
+function scdoc:parse_header(ln)
+ self.manpage, self.mansection = ln:match("^(%S*)%((%d*)%)")
+ if self.manpage:find("^apk%-") then
+ self.applet = self.manpage:sub(5):lower()
+ else
+ self.applet = self.manpage:upper()
+ end
+ self.parser = self.parse_default
+ self.section_text = self.nop
+end
+
+function scdoc:parse(fn)
+ self.parser = self.parse_header
+ for l in io.lines(fn) do
+ self:parser(l)
+ end
+end
+
+function scdoc:render_options(out, options)
+ local width = self.width
+ local nindent = 24
+
+ table.insert(out, ("%s options:\n"):format(upperfirst(options.name)))
+ for _, opt in ipairs(options) do
+ local indent = (" "):rep(nindent)
+ k, v = opt[1], opt[2]
+ if #k > nindent - 4 then
+ table.insert(out, (" %s\n"):format(k, "", v))
+ table.insert(out, indent)
+ else
+ local fmt = (" %%-%ds "):format(nindent - 4)
+ table.insert(out, fmt:format(k, v))
+ end
+
+ v = table.concat(v, " ")
+ local i = v:find("%.%s")
+ if not i then i = v:find("%.$") end
+ if i then v = v:sub(1, i-1) end
+ v = textwrap(v, width - nindent - 1)
+
+ table.insert(out, v[1])
+ table.insert(out, "\n")
+ for i = 2, #v do
+ table.insert(out, indent)
+ table.insert(out, v[i])
+ table.insert(out, "\n")
+ end
+ end
+end
+
+function scdoc:render_optgroups(out)
+ for _, options in ipairs(self.optgroup) do
+ if #options > 0 then
+ table.insert(out, options.name .. "\x00")
+ self:render_options(out, options)
+ if options.name == self.applet then
+ self:render_footer(out)
+ end
+ table.insert(out, "\x00")
+ end
+ end
+end
+
+function scdoc:render_footer(out)
+ table.insert(out, ("\nFor more information: man %s %s\n"):format(self.mansection, self.manpage))
+end
+
+function scdoc:render(out)
+ local width = self.width
+
+ if not self.applet then return end
+ table.insert(out, self.applet .. "\x00")
+ table.insert(out, table.concat(self.usage, "\n"))
+ table.insert(out, "\n")
+ if #self.commands > 0 then
+ for _, cmd in ipairs(self.commands) do
+ if type(cmd) == "string" then
+ table.insert(out, "\n" .. cmd .. ":\n")
+ else
+ table.insert(out, (" %-10s %s\n"):format(cmd[1], cmd[2]))
+ end
+ end
+ elseif #self.description > 0 then
+ table.insert(out, "\nDescription:\n")
+ for _, ln in ipairs(textwrap(table.concat(self.description, ' '), width - 2)) do
+ table.insert(out, (" %s\n"):format(ln))
+ end
+ end
+ if #self.notes > 0 then
+ table.insert(out, "\n")
+ table.insert(out, table.concat(self.notes, "\n"))
+ if self.manpage == "apk" then self:render_footer(out)
+ else table.insert(out, "\n") end
+ end
+ table.insert(out, "\x00")
+end
+
+local function compress(data)
+ local zlib = require 'zlib'
+ local level = 9
+ if type(zlib.version()) == "string" then
+ -- lua-lzlib interface
+ return zlib.compress(data, level)
+ else
+ -- lua-zlib interface
+ return zlib.deflate(level)(data, "finish")
+ end
+end
+
+local function dump_compressed_vars(name, data, header)
+ local width = 16
+ local cout = compress(data)
+ if header then print(header) end
+ print(("static const unsigned int uncompressed_%s_size = %d;"):format(name, #data))
+ print(("static const unsigned char compressed_%s[] = { /* %d bytes */"):format(name, #cout))
+ for i = 1, #cout do
+ if i % width == 1 then
+ io.write("\t")
+ end
+ --print(cout:byte(i))
+ io.write(("0x%02x,"):format(cout:byte(i)))
+ if i % width == 0 or i == #cout then
+ io.write("\n")
+ end
+ end
+ print("};")
+end
+
+local f = {}
+for _, fn in ipairs(arg) do
+ doc = setmetatable({
+ width = 78,
+ section = "HEADER",
+ usage = {},
+ description = {},
+ commands = {},
+ notes = {},
+ optgroup = {},
+ }, scdoc)
+ doc:parse(fn)
+ table.insert(f, doc)
+end
+table.sort(f, function(a, b) return a.applet < b.applet end)
+
+local out = {}
+for _, doc in ipairs(f) do doc:render(out) end
+for _, doc in ipairs(f) do doc:render_optgroups(out) end
+
+table.insert(out, "\x00")
+
+local help = table.concat(out)
+--io.stderr:write(help)
+dump_compressed_vars("help", help, "/* Automatically generated by genhelp.lua. Do not modify. */")