- add tools/scandoc utility to extract doc comments from source code - add qe-manual.c documentation framework - add qe-manual.md target for convenient repository documentation - document some utility functions - add strtod_c(str, endp) for const strings - add Makefile target qe-manual.md - bump VERSION to 5.3.4r
546 lines
15 KiB
C
546 lines
15 KiB
C
/****************************************************************
|
|
;
|
|
; SCANDOC.C (C) Copyright Charlie Gordon 2022
|
|
;
|
|
; Extract documentation from C source files
|
|
;
|
|
;****************************************************************/
|
|
|
|
/* A simple utility to preprocess C source code to extract
|
|
documentation comments. These comments start with an @ just after
|
|
the 2 initial characters of the comment.
|
|
They are ordered lexicographically.
|
|
The line starting with @ is removed from the output.
|
|
Macros are created with @MACRONAME=text till the enf of line
|
|
Macros are expanded before sorting the sections
|
|
*/
|
|
|
|
#include <ctype.h>
|
|
#include <errno.h>
|
|
#include <stdarg.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#define NAME "scandoc"
|
|
|
|
static int verbose;
|
|
|
|
static void *xmalloc(size_t size) {
|
|
void *p = malloc(size);
|
|
if (!p) {
|
|
fprintf(stderr, "%s: cannot allocate memory\n", NAME);
|
|
exit(1);
|
|
}
|
|
return p;
|
|
}
|
|
|
|
static int strstart(const char *str, const char *val, const char **ptr) {
|
|
size_t i;
|
|
for (i = 0; val[i] != '\0'; i++) {
|
|
if (str[i] != val[i])
|
|
return 0;
|
|
}
|
|
if (ptr)
|
|
*ptr = &str[i];
|
|
return 1;
|
|
}
|
|
|
|
static char *single_line(const char *s) {
|
|
/* convert the string `s` to a single line, replacing newlines and
|
|
adjacent white space with a single space and trimming initial and
|
|
trailing white space from the resulting string.
|
|
*/
|
|
char *res = strdup(s);
|
|
char *p = res;
|
|
char *q = res;
|
|
|
|
for (;;) {
|
|
while (isspace((unsigned char)*p))
|
|
p++;
|
|
while (*p && *p != '\n')
|
|
*q++ = *p++;
|
|
while (q > s && isspace((unsigned char)q[-1]))
|
|
*--q = '\0';
|
|
if (*p)
|
|
*q++ = ' ';
|
|
else
|
|
break;
|
|
}
|
|
*q = '\0';
|
|
return res;
|
|
}
|
|
|
|
typedef struct abbrev_t {
|
|
char *abbr;
|
|
size_t abbr_len;
|
|
char *rep;
|
|
size_t rep_len;
|
|
struct abbrev_t *next;
|
|
} abbrev_t;
|
|
|
|
static abbrev_t *abbrevs;
|
|
|
|
static abbrev_t *add_abbrev(const char *abbr, size_t abbr_len, const char *rep, size_t rep_len) {
|
|
abbrev_t *ap = xmalloc(sizeof *ap);
|
|
ap->abbr = strndup(abbr, abbr_len);
|
|
ap->abbr_len = strlen(ap->abbr);
|
|
ap->rep = strndup(rep, rep_len);
|
|
ap->rep_len = strlen(ap->rep);
|
|
ap->next = abbrevs;
|
|
return abbrevs = ap;
|
|
}
|
|
|
|
static abbrev_t *find_abbrev(const char *abbr, size_t abbr_len) {
|
|
abbrev_t *ap;
|
|
for (ap = abbrevs; ap; ap = ap->next) {
|
|
if (abbr_len == ap->abbr_len && !memcmp(abbr, ap->abbr, abbr_len))
|
|
break;
|
|
}
|
|
return ap;
|
|
}
|
|
|
|
typedef struct doc_t {
|
|
char *section;
|
|
char *text;
|
|
const char *filename;
|
|
int lineno;
|
|
} doc_t;
|
|
|
|
static doc_t *docs;
|
|
static int docs_len;
|
|
|
|
static int doc_strcmp(const char *p1, const char *p2) {
|
|
/* compare strings ignoring spaces and converting
|
|
numbers to implement natural ordering */
|
|
for (;;) {
|
|
unsigned char c1, c2;
|
|
while (isspace(c1 = *p1++))
|
|
continue;
|
|
while (isspace(c2 = *p2++))
|
|
continue;
|
|
if (isdigit(c1) && isdigit(c2)) {
|
|
long v1 = strtol(p1 - 1, (char **)(intptr_t)&p1, 10);
|
|
long v2 = strtol(p2 - 1, (char **)(intptr_t)&p2, 10);
|
|
if (v1 != v2)
|
|
return (v1 > v2) - (v2 > v1);
|
|
} else {
|
|
if (c1 != c2)
|
|
return c1 - c2;
|
|
if (c1 == '\0')
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
static int doc_compare(const void *a1, const void *a2) {
|
|
const doc_t *dp1 = a1;
|
|
const doc_t *dp2 = a2;
|
|
int cmp = doc_strcmp(dp1->section, dp2->section);
|
|
return cmp ? cmp : doc_strcmp(dp1->text, dp2->text);
|
|
}
|
|
|
|
static int flush_docs(const char *outname) {
|
|
FILE *ft = stdout;
|
|
int i;
|
|
|
|
if (outname) {
|
|
if ((ft = fopen(outname, "w")) == NULL) {
|
|
fprintf(stderr, "%s: cannot open output file %s: %s\n",
|
|
NAME, outname, strerror(errno));
|
|
return 1;
|
|
}
|
|
}
|
|
qsort(docs, docs_len, sizeof(*docs), doc_compare);
|
|
|
|
for (i = 0; i < docs_len; i++) {
|
|
doc_t *dp = &docs[i];
|
|
char *p = dp->text;
|
|
int indent, len;
|
|
|
|
/* remove initial indentation spaces from each line */
|
|
indent = strspn(p, " ");
|
|
if (p[indent] == '\0')
|
|
continue;
|
|
|
|
if (verbose) {
|
|
//fprintf(ft, "\n[//] # (%s:%d:%s)\n",
|
|
// dp->filename, dp->lineno, dp->section);
|
|
fprintf(ft, "\n<!---\n%s:%d:%s\n-->\n",
|
|
dp->filename, dp->lineno, dp->section);
|
|
}
|
|
fprintf(ft, "\n");
|
|
for (;;) {
|
|
int ii = strspn(p, " ");
|
|
if (p[ii] == '\0')
|
|
break;
|
|
if (ii > indent)
|
|
ii = indent;
|
|
p += ii;
|
|
len = strcspn(p, "\n");
|
|
fprintf(ft, "%.*s\n", len, p);
|
|
if (p[len] == '\n')
|
|
len++;
|
|
p += len;
|
|
}
|
|
}
|
|
if (ft != stdout)
|
|
fclose(ft);
|
|
return 0;
|
|
}
|
|
|
|
static int rtrim(const char *s, int len) {
|
|
while (len > 0 && isspace((unsigned char)s[len - 1]))
|
|
--len;
|
|
return len;
|
|
}
|
|
|
|
static int skipwhite(const char *s) {
|
|
int pos = 0;
|
|
while (isspace((unsigned char)s[pos]))
|
|
pos++;
|
|
return pos;
|
|
}
|
|
|
|
static void freep(char **pp) {
|
|
free(*pp);
|
|
*pp = NULL;
|
|
}
|
|
|
|
static void concat(char **pp, const char *p2, int len2, int strip) {
|
|
char *p1 = *pp;
|
|
char *p3;
|
|
int len1, len3, space = 0;
|
|
len1 = p1 ? strlen(p1) : 0;
|
|
if (len2 < 0)
|
|
len2 = strlen(p2);
|
|
if (strip) {
|
|
len1 = rtrim(p1, len1);
|
|
while (len2 > 0 && isspace((unsigned char)*p2)) {
|
|
p2++;
|
|
len2--;
|
|
}
|
|
len2 = rtrim(p2, len2);
|
|
space = (len1 && len2);
|
|
}
|
|
len3 = len1 + space + len2;
|
|
p3 = xmalloc(len3 + 1);
|
|
memcpy(p3, p1, len1);
|
|
if (space)
|
|
p3[len1] = ' ';
|
|
memcpy(p3 + len1 + space, p2, len2);
|
|
p3[len3] = '\0';
|
|
freep(pp);
|
|
*pp = p3;
|
|
}
|
|
|
|
__attribute__((format(printf, 2, 0)))
|
|
static int vxprintf(char **strp, const char *format, va_list ap)
|
|
{
|
|
char buf[1024];
|
|
va_list arg;
|
|
char *str;
|
|
int len;
|
|
|
|
if (!strp || !format) {
|
|
errno = EINVAL;
|
|
return -1;
|
|
}
|
|
|
|
va_copy(arg, ap);
|
|
len = vsnprintf(buf, sizeof buf, format, arg);
|
|
va_end(arg);
|
|
|
|
if (len < 0 || (str = malloc(len + 1)) == NULL)
|
|
return -1;
|
|
|
|
if (len < (int)sizeof(buf)) {
|
|
memcpy(str, buf, len + 1);
|
|
} else {
|
|
vsnprintf(str, len + 1, format, ap);
|
|
}
|
|
free(*strp);
|
|
*strp = str;
|
|
return len;
|
|
}
|
|
|
|
__attribute__((format(printf, 2, 3)))
|
|
static int xprintf(char **strp, const char *format, ...)
|
|
{
|
|
va_list ap;
|
|
int len;
|
|
|
|
va_start(ap, format);
|
|
len = vxprintf(strp, format, ap);
|
|
va_end(ap);
|
|
|
|
return len;
|
|
}
|
|
|
|
|
|
static void add_doc(const char *filename,
|
|
const char *comment, int lineno,
|
|
const char *proto_arg, int proto_line)
|
|
{
|
|
doc_t *dp;
|
|
char *sec;
|
|
char *text;
|
|
abbrev_t *ap;
|
|
int len = strlen(comment);
|
|
int line_len;
|
|
int abbr_len;
|
|
int text_len;
|
|
int sec_len;
|
|
|
|
if (len < 3 || comment[2] != '@')
|
|
return;
|
|
|
|
/* strip comment marks */
|
|
comment += 3;
|
|
len -= 3;
|
|
if (len > 1 && comment[len - 1] == '/' && comment[len - 2] == '*')
|
|
len -= 2;
|
|
|
|
/* strip boundary spaces */
|
|
while (len > 0 && *comment == ' ') {
|
|
comment++;
|
|
len--;
|
|
}
|
|
len = rtrim(comment, len);
|
|
|
|
for (line_len = 0; line_len < len && comment[line_len] != '\n'; line_len++)
|
|
continue;
|
|
|
|
/* extract section (first line) */
|
|
sec_len = rtrim(comment, line_len);
|
|
sec = strndup(comment, sec_len);
|
|
|
|
if (line_len < len && comment[line_len] == '\n')
|
|
line_len++;
|
|
text_len = len - line_len;
|
|
text = strndup(comment + line_len, text_len);
|
|
|
|
if (verbose) {
|
|
fprintf(stderr, "%s:%d:%s\n", filename, lineno, sec);
|
|
if (verbose > 1) {
|
|
fprintf(stderr, "%s\n", text);
|
|
}
|
|
}
|
|
|
|
/* scan the first word */
|
|
abbr_len = strcspn(sec, " =");
|
|
if (sec[abbr_len] == '=') {
|
|
/* macro definition */
|
|
char *rep = sec + abbr_len + 1;
|
|
/* store the expansion for the macro name */
|
|
ap = add_abbrev(sec, abbr_len, rep, -1);
|
|
/* remove the macro name and equal sign from the section line */
|
|
xprintf(&sec, "%s", rep);
|
|
} else
|
|
if ((ap = find_abbrev(sec, abbr_len)) != NULL) {
|
|
/* expand the macro inside in the section line */
|
|
xprintf(&sec, "%s%s", ap->rep, sec + abbr_len);
|
|
}
|
|
/* synthesize extra data based on tag */
|
|
if (ap) {
|
|
if (!strcmp(ap->abbr, "API") && proto_arg) {
|
|
int pos, epos, elen;
|
|
char *proto;
|
|
|
|
/* skip storage class from prototype */
|
|
strstart(proto_arg, "static ", &proto_arg);
|
|
strstart(proto_arg, "inline ", &proto_arg);
|
|
strstart(proto_arg, "extern ", &proto_arg);
|
|
/* linearize the prototype */
|
|
proto = single_line(proto_arg);
|
|
|
|
/* extract function name */
|
|
for (pos = epos = elen = 0; proto[pos]; pos++) {
|
|
if (proto[pos] == '(') {
|
|
elen = pos - epos;
|
|
break;
|
|
}
|
|
if (proto[pos] == ' ' || proto[pos] == '*') {
|
|
epos = pos + 1;
|
|
}
|
|
}
|
|
for (; proto[pos]; pos++) {
|
|
if (proto[pos] == ';' || proto[pos] == '{') {
|
|
break;
|
|
}
|
|
}
|
|
pos = rtrim(proto, pos);
|
|
/* entry name starts at epos, length is elen */
|
|
concat(&sec, proto + epos, elen, 1);
|
|
xprintf(&text, "%*s### `%.*s;`\n\n%s",
|
|
skipwhite(text), "", pos, proto, text);
|
|
|
|
/* should implement more elaborate text reformating */
|
|
/* handle function macros */
|
|
const char *p = text;
|
|
while ((p = strchr(p, '@')) != NULL) {
|
|
int offset = p - text;
|
|
if (strstart(p, "@argument", &p) || strstart(p, "@param", &p)) {
|
|
xprintf(&text, "%.*s\n* argument%s", offset, text, p);
|
|
} else
|
|
if (strstart(p, "@returns", &p) || strstart(p, "@return", &p)) {
|
|
xprintf(&text, "%.*s\nReturn%s", offset, text, p);
|
|
} else
|
|
if (strstart(p, "@note:", &p) || strstart(p, "@note", &p)) {
|
|
xprintf(&text, "%.*s\nNote:%s", offset, text, p);
|
|
} else
|
|
if (strstart(p, "@seealso", &p) || strstart(p, "@see_also", &p)) {
|
|
xprintf(&text, "%.*s\nSee also:%s", offset, text, p);
|
|
}
|
|
p = text + offset + 1;
|
|
}
|
|
free(proto);
|
|
} else
|
|
{
|
|
}
|
|
}
|
|
|
|
/* append section */
|
|
docs = realloc(docs, (docs_len + 1) * sizeof(*docs));
|
|
if (!docs) {
|
|
fprintf(stderr, "%s: cannot allocate memory\n", NAME);
|
|
exit(1);
|
|
}
|
|
dp = &docs[docs_len++];
|
|
dp->section = sec;
|
|
dp->text = text;
|
|
dp->filename = filename;
|
|
dp->lineno = lineno;
|
|
}
|
|
|
|
static int scandoc(const char *filename, FILE *fp) {
|
|
char buf[512];
|
|
int len, lineno = 0;
|
|
int in_comment = 0, comment_line = 0;
|
|
int in_proto = 0, proto_line = 0;
|
|
char *proto = NULL;
|
|
char *comment = NULL;
|
|
char *p;
|
|
|
|
while (fgets(buf, sizeof buf, fp)) {
|
|
if (in_proto == 2 && in_comment == 2) {
|
|
add_doc(filename, comment, comment_line, proto, proto_line);
|
|
freep(&comment);
|
|
freep(&proto);
|
|
in_comment = in_proto = 0;
|
|
}
|
|
lineno++;
|
|
p = buf + skipwhite(buf);
|
|
if (in_proto != 1) {
|
|
char *p1 = buf;
|
|
if (!strncmp(p, "/*@", 3)) {
|
|
if (in_comment == 2) {
|
|
/* flush pending doc comment */
|
|
add_doc(filename, comment, comment_line, proto, proto_line);
|
|
freep(&comment);
|
|
freep(&proto);
|
|
in_comment = in_proto = 0;
|
|
}
|
|
freep(&comment);
|
|
comment_line = lineno;
|
|
in_comment = 1;
|
|
p1 = p;
|
|
}
|
|
int p1_len = strlen(p1);
|
|
if (in_comment == 1) {
|
|
char *e = strstr(p1, "*/");
|
|
if (e) {
|
|
p1_len = e + 2 - p1;
|
|
in_comment = 2;
|
|
}
|
|
concat(&comment, p1, p1_len, 0);
|
|
continue;
|
|
}
|
|
if (!*p) {
|
|
/* blank line */
|
|
continue;
|
|
}
|
|
if (p == buf && *p != '/' && *p != '#' && *p != '}' && *p != '*') {
|
|
/* discard previous proto */
|
|
freep(&proto);
|
|
proto_line = lineno;
|
|
in_proto = 1;
|
|
}
|
|
}
|
|
if (in_proto == 1) {
|
|
if (!*p) {
|
|
/* blank line */
|
|
continue;
|
|
}
|
|
len = strcspn(p, ";{");
|
|
if (p[len] == '{' || p[len] == ';') {
|
|
in_proto = 2;
|
|
}
|
|
concat(&proto, p, len, 1);
|
|
}
|
|
}
|
|
if (in_comment == 2) {
|
|
add_doc(filename, comment, comment_line, proto, proto_line);
|
|
}
|
|
freep(&comment);
|
|
freep(&proto);
|
|
return 0;
|
|
}
|
|
|
|
int main(int argc, char *argv[]) {
|
|
char *filename = NULL;
|
|
char *outname = NULL;
|
|
int i;
|
|
|
|
for (i = 1; i < argc; i++) {
|
|
char *arg = argv[i];
|
|
if (*arg == '-') {
|
|
if (arg[1] == '-') {
|
|
if (!strcmp(arg, "--help"))
|
|
goto usage;
|
|
fprintf(stderr, "%s: bad option: -%s\n", NAME, arg);
|
|
return 1;
|
|
}
|
|
while (*++arg) {
|
|
switch (*arg) {
|
|
case 'h':
|
|
case '?':
|
|
usage:
|
|
printf("usage: %s [-v] [-o FILENAME] [FILE] ...\n", NAME);
|
|
return 2;
|
|
case 'o':
|
|
outname = arg[1] ? arg + 1 : argv[++i];
|
|
if (!outname) {
|
|
fprintf(stderr, "%s: missing filename for -o\n", NAME);
|
|
return 1;
|
|
}
|
|
break;
|
|
case 'v':
|
|
verbose++;
|
|
continue;
|
|
default:
|
|
fprintf(stderr, "%s: bad option: %s\n", NAME, arg);
|
|
return 1;
|
|
}
|
|
break;
|
|
}
|
|
} else {
|
|
FILE *fp;
|
|
filename = arg;
|
|
if ((fp = fopen(filename, "r")) == NULL) {
|
|
fprintf(stderr, "%s: cannot open input file %s: %s\n",
|
|
NAME, filename, strerror(errno));
|
|
return 1;
|
|
}
|
|
scandoc(filename, fp);
|
|
fclose(fp);
|
|
}
|
|
}
|
|
if (!filename) {
|
|
scandoc("<stdin>", stdin);
|
|
}
|
|
/* output the documentation to outname or stdout */
|
|
flush_docs(outname);
|
|
return 0;
|
|
}
|