From 5258b484bf13441d3d5c199e3c8d70cf2a75b3ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20Ter=C3=A4s?= Date: Fri, 24 Apr 2020 11:49:14 +0300 Subject: [PATCH] add script to autogenerate help from man pages This creates main help like: -- usage: apk [...] COMMAND [...] 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 [...] 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 -- --- .gitlab-ci.yml | 5 +- libfetch/Makefile | 6 +- src/Makefile | 15 ++- src/apk.c | 75 ++---------- src/apk_applet.h | 10 +- src/app_add.c | 4 +- src/app_audit.c | 1 - src/app_cache.c | 2 - src/app_del.c | 2 - src/app_dot.c | 2 - src/app_fetch.c | 4 +- src/app_fix.c | 2 - src/app_index.c | 10 +- src/app_info.c | 2 - src/app_list.c | 2 - src/app_manifest.c | 2 - src/app_policy.c | 1 - src/app_search.c | 2 - src/app_update.c | 1 - src/app_upgrade.c | 1 - src/app_verify.c | 2 - src/app_version.c | 2 +- src/genhelp.lua | 300 +++++++++++++++++++++++++++++++++++++++++++++ src/help.c | 43 +++++++ 24 files changed, 385 insertions(+), 111 deletions(-) create mode 100644 src/genhelp.lua create mode 100644 src/help.c diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1201d83..1b97a01 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -6,17 +6,18 @@ test:alpine: stage: test script: - apk update - - apk add make gcc git musl-dev openssl-dev linux-headers zlib-dev lua5.3-dev + - apk add make gcc git musl-dev openssl-dev linux-headers zlib-dev lua5.3-dev lua5.3-lzlib - make -j$(nproc) check tags: - docker-alpine + - x86_64 test:debian: image: debian stage: test script: - apt-get update - - apt-get install -y make gcc git libssl-dev zlib1g-dev lua5.3-dev sudo + - apt-get install -y make gcc git libssl-dev zlib1g-dev lua5.3-dev lua5.2 lua-zlib-dev sudo - unlink /bin/sh - ln -s /bin/bash /bin/sh - make -j$(nproc) check diff --git a/libfetch/Makefile b/libfetch/Makefile index 12fead9..c4b56f5 100644 --- a/libfetch/Makefile +++ b/libfetch/Makefile @@ -5,7 +5,7 @@ CFLAGS_common.o += -DCA_CERT_FILE=\"$(CONFDIR)/ca.pem\" -DCA_CRL_FILE=\"$(CONFD CFLAGS_common.o += -DCLIENT_CERT_FILE=\"$(CONFDIR)/cert.pem\" -DCLIENT_KEY_FILE=\"$(CONFDIR)/cert.key\" quiet_cmd_generr = GENERR $@ - cmd_generr = $(obj)/errlist.sh $(basename $( $@ + cmd_generr = $(src)/errlist.sh $(basename $( $@ -$(obj)/%err.h: $(obj)/%.errors - @$(call echo-cmd,generr) $(cmd_generr); $(cmd_generr) +$(obj)/%err.h: $(src)/%.errors + @$(call echo-cmd,generr) $(cmd_generr) diff --git a/src/Makefile b/src/Makefile index 37c29a2..bea4cdf 100644 --- a/src/Makefile +++ b/src/Makefile @@ -1,6 +1,8 @@ PKG_CONFIG ?= pkg-config LUAAPK ?= yes +LUA ?= $(firstword $(wildcard /usr/bin/lua5.3 /usr/bin/lua5.2)) + OPENSSL_CFLAGS := $(shell $(PKG_CONFIG) --cflags openssl) OPENSSL_LIBS := $(shell $(PKG_CONFIG) --libs openssl) @@ -52,7 +54,7 @@ endif # Apk utility progs-y += apk -apk-objs := apk.o \ +apk-objs := apk.o help.o \ app_add.o app_del.o app_fix.o app_update.o app_upgrade.o \ app_info.o app_list.o app_search.o app_manifest.o \ app_policy.o app_stats.o \ @@ -82,6 +84,17 @@ LIBS := -Wl,--as-needed \ $(OPENSSL_LIBS) $(ZLIB_LIBS) \ -Wl,--no-as-needed +# Help generation +quiet_cmd_genhelp = GENHELP $@ + cmd_genhelp = $(LUA) $(src)/genhelp.lua $(filter %.scd, $^) > $@ + +$(obj)/help.h: $(src)/genhelp.lua $(wildcard doc/apk*.8.scd) + @$(call echo-cmd,genhelp) $(cmd_genhelp) + +CFLAGS_help.o := -I$(obj) + +generate-y += help.h + # Test build ifeq ($(TEST),y) progs-y += apk-test diff --git a/src/apk.c b/src/apk.c index 68050cd..e993ac6 100644 --- a/src/apk.c +++ b/src/apk.c @@ -192,8 +192,8 @@ static int option_parse_global(void *ctx, struct apk_db_options *dbopts, int opt static const struct apk_option options_global[] = { { 'h', "help" }, - { 'p', "root", required_argument, "DIR" }, - { 'X', "repository", required_argument, "REPO" }, + { 'p', "root", required_argument }, + { 'X', "repository", required_argument }, { 'q', "quiet" }, { 'v', "verbose" }, { 'i', "interactive" }, @@ -207,23 +207,23 @@ static const struct apk_option options_global[] = { { 0x123, "force-refresh" }, { 'U', "update-cache" }, { 0x101, "progress" }, - { 0x10f, "progress-fd", required_argument, "FD" }, + { 0x10f, "progress-fd", required_argument }, { 0x110, "no-progress" }, { 0x106, "purge" }, { 0x103, "allow-untrusted" }, - { 0x105, "wait", required_argument, "TIME" }, - { 0x107, "keys-dir", required_argument, "KEYSDIR" }, - { 0x108, "repositories-file", required_argument, "REPOFILE" }, + { 0x105, "wait", required_argument }, + { 0x107, "keys-dir", required_argument }, + { 0x108, "repositories-file", required_argument }, { 0x109, "no-network" }, { 0x115, "no-cache" }, - { 0x116, "cache-dir", required_argument, "CACHEDIR" }, - { 0x119, "cache-max-age", required_argument, "AGE" }, - { 0x112, "arch", required_argument, "ARCH" }, + { 0x116, "cache-dir", required_argument }, + { 0x119, "cache-max-age", required_argument }, + { 0x112, "arch", required_argument }, { 0x114, "print-arch" }, #ifdef TEST_MODE - { 0x200, "test-repo", required_argument, "REPO" }, - { 0x201, "test-instdb", required_argument, "INSTALLED" }, - { 0x202, "test-world", required_argument, "WORLD DEPS" }, + { 0x200, "test-repo", required_argument }, + { 0x201, "test-instdb", required_argument }, + { 0x202, "test-world", required_argument }, #endif }; @@ -280,59 +280,10 @@ const struct apk_option_group optgroup_commit = { .parse = option_parse_commit, }; -static int format_option(char *buf, size_t len, const struct apk_option *o, - const char *separator) -{ - int i = 0; - - if (o->val <= 0xff && isalnum(o->val)) { - i += snprintf(&buf[i], len - i, "-%c", o->val); - if (o->name != NULL) - i += snprintf(&buf[i], len - i, "%s", separator); - } - if (o->name != NULL) - i += snprintf(&buf[i], len - i, "--%s", o->name); - if (o->arg_name != NULL) - i += snprintf(&buf[i], len - i, " %s", o->arg_name); - - return i; -} - -static void print_usage(const char *cmd, const char *args, const struct apk_option_group **optgroups) -{ - struct apk_indent indent = { .indent = 11 }; - const struct apk_option *opts; - char word[128]; - int g, i, j; - - indent.x = printf("\nusage: apk %s", cmd) - 1; - for (g = 0; optgroups[g]; g++) { - opts = optgroups[g]->options; - for (i = 0; i < optgroups[g]->num_options; i++) { - if (!opts[i].name) continue; - j = 0; - word[j++] = '['; - j += format_option(&word[j], sizeof(word) - j, &opts[i], "|"); - word[j++] = ']'; - apk_print_indented(&indent, APK_BLOB_PTR_LEN(word, j)); - } - } - if (args != NULL) - apk_print_indented(&indent, APK_BLOB_STR(args)); - printf("\n"); -} - static int usage(struct apk_applet *applet) { version(); - if (applet == NULL) { - print_usage("", "[...]", default_optgroups); - } else { - print_usage(applet->name, applet->arguments, &applet->optgroups[1]); - } - - printf("\nThis apk has coffee making abilities.\n"); - + apk_help(applet); return 1; } diff --git a/src/apk_applet.h b/src/apk_applet.h index 1c637a6..390ec59 100644 --- a/src/apk_applet.h +++ b/src/apk_applet.h @@ -17,16 +17,10 @@ #include "apk_defines.h" #include "apk_database.h" -#define APK_COMMAND_GROUP_INSTALL 0x0001 -#define APK_COMMAND_GROUP_SYSTEM 0x0002 -#define APK_COMMAND_GROUP_QUERY 0x0004 -#define APK_COMMAND_GROUP_REPO 0x0008 - struct apk_option { int val; const char *name; int has_arg; - const char *arg_name; }; struct apk_option_group { @@ -42,10 +36,9 @@ struct apk_applet { struct list_head node; const char *name; - const char *arguments; const struct apk_option_group *optgroups[4]; - unsigned int open_flags, forced_flags, forced_force, command_groups; + unsigned int open_flags, forced_flags, forced_force; int context_size; int (*main)(void *ctx, struct apk_database *db, struct apk_string_array *args); @@ -53,6 +46,7 @@ struct apk_applet { extern const struct apk_option_group optgroup_global, optgroup_commit; +void apk_help(struct apk_applet *applet); void apk_applet_register(struct apk_applet *); typedef void (*apk_init_func_t)(void); diff --git a/src/app_add.c b/src/app_add.c index f5edae0..1e1fe00 100644 --- a/src/app_add.c +++ b/src/app_add.c @@ -54,7 +54,7 @@ static const struct apk_option options_applet[] = { { 0x10001, "no-chown" }, { 'u', "upgrade" }, { 'l', "latest" }, - { 't', "virtual", required_argument, "NAME" }, + { 't', "virtual", required_argument }, }; static const struct apk_option_group optgroup_applet = { @@ -204,9 +204,7 @@ static int add_main(void *ctx, struct apk_database *db, struct apk_string_array static struct apk_applet apk_add = { .name = "add", - .arguments = "PACKAGE...", .open_flags = APK_OPENF_WRITE, - .command_groups = APK_COMMAND_GROUP_INSTALL, .context_size = sizeof(struct add_ctx), .optgroups = { &optgroup_global, &optgroup_commit, &optgroup_applet }, .main = add_main, diff --git a/src/app_audit.c b/src/app_audit.c index 696599b..f2df583 100644 --- a/src/app_audit.c +++ b/src/app_audit.c @@ -346,7 +346,6 @@ static int audit_main(void *ctx, struct apk_database *db, struct apk_string_arra static struct apk_applet apk_audit = { .name = "audit", - .arguments = "[directory to audit]...", .open_flags = APK_OPENF_READ|APK_OPENF_NO_SCRIPTS|APK_OPENF_NO_REPOS, .context_size = sizeof(struct audit_ctx), .optgroups = { &optgroup_global, &optgroup_applet }, diff --git a/src/app_cache.c b/src/app_cache.c index 7ea5356..b499ad2 100644 --- a/src/app_cache.c +++ b/src/app_cache.c @@ -182,9 +182,7 @@ err: static struct apk_applet apk_cache = { .name = "cache", - .arguments = "sync | clean | download", .open_flags = APK_OPENF_READ|APK_OPENF_NO_SCRIPTS|APK_OPENF_CACHE_WRITE, - .command_groups = APK_COMMAND_GROUP_SYSTEM, .context_size = sizeof(struct cache_ctx), .optgroups = { &optgroup_global, &optgroup_applet }, .main = cache_main, diff --git a/src/app_del.c b/src/app_del.c index a5e8ddd..a978f10 100644 --- a/src/app_del.c +++ b/src/app_del.c @@ -167,9 +167,7 @@ static int del_main(void *pctx, struct apk_database *db, struct apk_string_array static struct apk_applet apk_del = { .name = "del", - .arguments = "PACKAGE...", .open_flags = APK_OPENF_WRITE | APK_OPENF_NO_AUTOUPDATE, - .command_groups = APK_COMMAND_GROUP_INSTALL, .context_size = sizeof(struct del_ctx), .optgroups = { &optgroup_global, &optgroup_commit, &optgroup_applet }, .main = del_main, diff --git a/src/app_dot.c b/src/app_dot.c index b85d69b..d567e5a 100644 --- a/src/app_dot.c +++ b/src/app_dot.c @@ -168,9 +168,7 @@ static int dot_main(void *pctx, struct apk_database *db, struct apk_string_array static struct apk_applet apk_dot = { .name = "dot", - .arguments = "PKGMASK...", .open_flags = APK_OPENF_READ | APK_OPENF_NO_STATE, - .command_groups = APK_COMMAND_GROUP_QUERY, .context_size = sizeof(struct dot_ctx), .optgroups = { &optgroup_global, &optgroup_applet }, .main = dot_main, diff --git a/src/app_fetch.c b/src/app_fetch.c index 677fabf..224618b 100644 --- a/src/app_fetch.c +++ b/src/app_fetch.c @@ -96,7 +96,7 @@ static const struct apk_option options_applet[] = { { 'R', "recursive" }, { 0x104, "simulate" }, { 's', "stdout" }, - { 'o', "output", required_argument, "DIR" }, + { 'o', "output", required_argument }, }; static const struct apk_option_group optgroup_applet = { @@ -343,9 +343,7 @@ static int fetch_main(void *pctx, struct apk_database *db, struct apk_string_arr static struct apk_applet apk_fetch = { .name = "fetch", - .arguments = "PACKAGE...", .open_flags = APK_OPENF_READ | APK_OPENF_NO_STATE, - .command_groups = APK_COMMAND_GROUP_REPO, .context_size = sizeof(struct fetch_ctx), .optgroups = { &optgroup_global, &optgroup_applet }, .main = fetch_main, diff --git a/src/app_fix.c b/src/app_fix.c index ed1ff76..6f332af 100644 --- a/src/app_fix.c +++ b/src/app_fix.c @@ -115,9 +115,7 @@ static int fix_main(void *pctx, struct apk_database *db, struct apk_string_array static struct apk_applet apk_fix = { .name = "fix", - .arguments = "PACKAGE...", .open_flags = APK_OPENF_WRITE, - .command_groups = APK_COMMAND_GROUP_SYSTEM, .context_size = sizeof(struct fix_ctx), .optgroups = { &optgroup_global, &optgroup_commit, &optgroup_applet }, .main = fix_main, diff --git a/src/app_index.c b/src/app_index.c index 3e3e437..094388b 100644 --- a/src/app_index.c +++ b/src/app_index.c @@ -56,10 +56,10 @@ static int option_parse_applet(void *ctx, struct apk_db_options *dbopts, int opt } static const struct apk_option options_applet[] = { - { 'o', "output", required_argument, "FILE" }, - { 'x', "index", required_argument, "INDEX" }, - { 'd', "description", required_argument, "TEXT" }, - { 0x10000, "rewrite-arch", required_argument, "ARCH" }, + { 'o', "output", required_argument }, + { 'x', "index", required_argument }, + { 'd', "description", required_argument }, + { 0x10000, "rewrite-arch", required_argument }, }; static const struct apk_option_group optgroup_applet = { @@ -253,9 +253,7 @@ static int index_main(void *ctx, struct apk_database *db, struct apk_string_arra static struct apk_applet apk_index = { .name = "index", - .arguments = "FILE...", .open_flags = APK_OPENF_READ | APK_OPENF_NO_STATE | APK_OPENF_NO_REPOS, - .command_groups = APK_COMMAND_GROUP_REPO, .context_size = sizeof(struct index_ctx), .optgroups = { &optgroup_global, &optgroup_applet }, .main = index_main, diff --git a/src/app_info.c b/src/app_info.c index ee9a0aa..9b2bcb1 100644 --- a/src/app_info.c +++ b/src/app_info.c @@ -476,9 +476,7 @@ static const struct apk_option_group optgroup_applet = { static struct apk_applet apk_info = { .name = "info", - .arguments = "PACKAGE...", .open_flags = APK_OPENF_READ, - .command_groups = APK_COMMAND_GROUP_QUERY, .context_size = sizeof(struct info_ctx), .optgroups = { &optgroup_global, &optgroup_applet }, .main = info_main, diff --git a/src/app_list.c b/src/app_list.c index 3315ea3..f46d30f 100644 --- a/src/app_list.c +++ b/src/app_list.c @@ -261,9 +261,7 @@ static int list_main(void *pctx, struct apk_database *db, struct apk_string_arra static struct apk_applet apk_list = { .name = "list", - .arguments = "PATTERN", .open_flags = APK_OPENF_READ, - .command_groups = APK_COMMAND_GROUP_QUERY, .context_size = sizeof(struct list_ctx), .optgroups = { &optgroup_global, &optgroup_applet }, .main = list_main, diff --git a/src/app_manifest.c b/src/app_manifest.c index fe64bea..757c8a9 100644 --- a/src/app_manifest.c +++ b/src/app_manifest.c @@ -124,9 +124,7 @@ static int manifest_main(void *ctx, struct apk_database *db, struct apk_string_a static struct apk_applet apk_manifest = { .name = "manifest", - .arguments = "PACKAGE...", .open_flags = APK_OPENF_READ, - .command_groups = APK_COMMAND_GROUP_REPO, .main = manifest_main, }; diff --git a/src/app_policy.c b/src/app_policy.c index 2d6ae4f..4992858 100644 --- a/src/app_policy.c +++ b/src/app_policy.c @@ -71,7 +71,6 @@ static int policy_main(void *ctx, struct apk_database *db, struct apk_string_arr static struct apk_applet apk_policy = { .name = "policy", .open_flags = APK_OPENF_READ, - .command_groups = APK_COMMAND_GROUP_QUERY, .main = policy_main, }; diff --git a/src/app_search.c b/src/app_search.c index 9bf64b2..5d70432 100644 --- a/src/app_search.c +++ b/src/app_search.c @@ -206,9 +206,7 @@ static int search_main(void *pctx, struct apk_database *db, struct apk_string_ar static struct apk_applet apk_search = { .name = "search", - .arguments = "PATTERN", .open_flags = APK_OPENF_READ | APK_OPENF_NO_STATE, - .command_groups = APK_COMMAND_GROUP_QUERY, .context_size = sizeof(struct search_ctx), .optgroups = { &optgroup_global, &optgroup_applet }, .main = search_main, diff --git a/src/app_update.c b/src/app_update.c index bbb2b09..f79655e 100644 --- a/src/app_update.c +++ b/src/app_update.c @@ -49,7 +49,6 @@ static struct apk_applet apk_update = { .name = "update", .open_flags = APK_OPENF_WRITE, .forced_force = APK_FORCE_REFRESH, - .command_groups = APK_COMMAND_GROUP_SYSTEM, .main = update_main, }; diff --git a/src/app_upgrade.c b/src/app_upgrade.c index 02b1ae7..3f7f096 100644 --- a/src/app_upgrade.c +++ b/src/app_upgrade.c @@ -188,7 +188,6 @@ static int upgrade_main(void *ctx, struct apk_database *db, struct apk_string_ar static struct apk_applet apk_upgrade = { .name = "upgrade", .open_flags = APK_OPENF_WRITE, - .command_groups = APK_COMMAND_GROUP_SYSTEM, .context_size = sizeof(struct upgrade_ctx), .optgroups = { &optgroup_global, &optgroup_commit, &optgroup_applet }, .main = upgrade_main, diff --git a/src/app_verify.c b/src/app_verify.c index 296253c..264610f 100644 --- a/src/app_verify.c +++ b/src/app_verify.c @@ -48,9 +48,7 @@ static int verify_main(void *ctx, struct apk_database *db, struct apk_string_arr static struct apk_applet apk_verify = { .name = "verify", - .arguments = "FILE...", .open_flags = APK_OPENF_READ | APK_OPENF_NO_STATE, - .command_groups = APK_COMMAND_GROUP_REPO, .forced_flags = APK_ALLOW_UNTRUSTED, .main = verify_main, }; diff --git a/src/app_version.c b/src/app_version.c index a29d899..33d787e 100644 --- a/src/app_version.c +++ b/src/app_version.c @@ -100,7 +100,7 @@ static const struct apk_option options_applet[] = { { 't', "test" }, { 'c', "check" }, { 'a', "all" }, - { 'l', "limit", required_argument, "LIMCHARs" }, + { 'l', "limit", required_argument }, }; static const struct apk_option_group optgroup_applet = { 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. */") diff --git a/src/help.c b/src/help.c new file mode 100644 index 0000000..a27992a --- /dev/null +++ b/src/help.c @@ -0,0 +1,43 @@ +/* help.c - Alpine Package Keeper (APK) + * + * Copyright (C) 2020 Timo Teräs + * All rights reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 as published + * by the Free Software Foundation. See http://www.gnu.org/ for details. + */ + +#include +#include "apk_applet.h" +#include "apk_print.h" + +static int is_group(struct apk_applet *applet, const char *topic) +{ + if (!applet) return strcasecmp(topic, "apk") == 0; + if (strcasecmp(topic, applet->name) == 0) return 1; + for (int i = 0; applet->optgroups[i] && i < ARRAY_SIZE(applet->optgroups); i++) + if (strcasecmp(applet->optgroups[i]->name, topic) == 0) return 1; + return 0; +} + +void apk_help(struct apk_applet *applet) +{ +#include "help.h" + + char buf[uncompressed_help_size], *ptr, *msg; + unsigned long len = sizeof buf; + int num = 0; + + uncompress((unsigned char*) buf, &len, compressed_help, sizeof compressed_help); + for (ptr = buf; *ptr && ptr < &buf[len]; ptr = msg + strlen(msg) + 1) { + msg = ptr + strlen(ptr) + 1; + + if (is_group(applet, ptr)) { + fputc('\n', stdout); + fwrite(msg, strlen(msg), 1, stdout); + num++; + } + } + if (num == 0) apk_error("Help not found"); +}