--- Makefile | 5 + README | 1 + config.def.h | 19 ++ man.1 | 154 ++++++++++++++ man.c | 677 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 856 insertions(+) create mode 100644 config.def.h create mode 100644 man.1 create mode 100644 man.c diff --git a/Makefile b/Makefile index 9ec9990..1e507f9 100644 --- a/Makefile +++ b/Makefile _AT_@ -6,6 +6,7 @@ include config.mk HDR =\ arg.h\ compat.h\ + config.h\ crypt.h\ fs.h\ md5.h\ _AT_@ -121,6 +122,7 @@ BIN =\ logger\ logname\ ls\ + man\ md5sum\ mkdir\ mkfifo\ _AT_@ -192,6 +194,9 @@ $(BIN): $(LIB) $(@:=.o) $(OBJ): $(HDR) config.mk +config.h: + cp config.def.h $_AT_ + .o: $(CC) $(LDFLAGS) -o $_AT_ $< $(LIB) diff --git a/README b/README index da2e500..e518796 100644 --- a/README +++ b/README _AT_@ -50,6 +50,7 @@ The following tools are implemented: 0=*|o logger . 0=*|o logname . 0#* o ls (-C, -k, -m, -p, -s, -x) +0#* o man (-k) 0=*|x md5sum . 0=*|o mkdir . 0=*|o mkfifo . diff --git a/config.def.h b/config.def.h new file mode 100644 index 0000000..14e1440 --- /dev/null +++ b/config.def.h _AT_@ -0,0 +1,19 @@ +/* See LICENSE file for copyright and license details. */ + +#define GROFF "/usr/bin/groff" + +#define GUNZIP "exec /usr/bin/gunzip" +#define UNCOMPRESS "exec /usr/bin/uncompress" +#define BUNZIP2 "exec /usr/bin/bunzip2" +#define UNXZ "exec /usr/bin/unxz" +#define UNLZMA "exec /usr/bin/unlzma" +#define LUNZIP "exec /usr/bin/lzip -d" +#define UNLZ4 "exec /usr/bin/unlz4" + +#define MANPATHTRANS "/=/usr" +#define MANPATH "/usr/local/share/man:/usr/share/man" +#define MANSECT "3p:3:0:2:1:8:n:l:5:4:6:7:x:9:a:b:c:d:e:f:g:h:i:j:k:l:m:o:p:q:r:s:t:u:v:w:u:y:z" +#define MANBINSECT "1:8" +#define MANETCSECT "5" +#define MANCOMP "gz="GUNZIP":z="GUNZIP":Z="UNCOMPRESS":bz2="BUNZIP2":xz="UNXZ":lzma="UNLZMA":lz="LUNZIP":lz4="UNLZ4 +#define MANPAGER "exec /usr/bin/less" diff --git a/man.1 b/man.1 new file mode 100644 index 0000000..9985b2b --- /dev/null +++ b/man.1 _AT_@ -0,0 +1,154 @@ +.Dd 2017-02-01 +.Dt MAN 1 +.Os sbase +.Sh NAME +.Nm man +.Nd display online reference manuals +.Sh SYNOPSIS +.Nm +.Op Fl 7 | Fl 8 +.Op Ar section +.Ar name +.Op Ar subcommand ... +.Sh DESCRIPTION +.Nm +displays the manual with the selected +.Ar name . +.Ar name +is usually a command, C function or system call. +All +.Ar subcommand +are appended to +.Ar name +with \fB-\fP joining the arguments. +Multiple manuals have the same name, in this case +.Ar section +can be used to specify which of them to display. +.P +The table below shows common +.Ar section +numbers. +.Bd -literal -offset left +1 Executable commands, as implemented +1p Executable commands, as specified by POSIX +2 System calls +3 Library calls, as implemented +3p Library calls, as specified by POSIX +4 Special files +5 Configuration files, file formats and conventions +6 Entertainment +7 Miscellanea +8 System administration commands +9 Kernel routines +0 Header files, as implemented +0p Header files, as specified by POSIX +l Tcl/Tk commands +n SQL commands +.Ed +.Sh OPTIONS +.Bl -tag -width Ds +.It Fl 7 +Convert output to ASCII. +.It Fl 8 +Allow to UTF-8. +.El +.Sh ENVIRONMENT VARIABLES +.Bl -tag -width Ds +.It Ev MANPATH +Colon-separated list of where manual are stored. +Each listed directory shall directions for each +section rather than the manuals themself. +.It Ev MANPATHTRANS +Colon-separated list of directory remappings used +to find the manual when command is given by its +path. Each entry is of the format +.Va bindir Ns Cm = Ns Va mandir . +If the parent directory of the directory +.Ar name +matches +.Va bindir , +.Va mandir +is used as +.Ar MANPATH . +.It Ev MANSECT +Colon-separated list of +.Ar section +numbers. If a manual is found under two or more +sections, the first named section in +.Ev MANSECT +is selected. +.It Ev MANBINSECT +Similar to +.Ev MANSECT , +but is used when +.Ar name +is the path to an executable file. +.It Ev MANETCSECT , +but is used when +.Ar name +is the path to file in +.Pa /etc . +.It Ev MANCOMP +Colon-separated list of compressions. Each entry +is formated as +.Va extension Ns Cm = Ns Va command , +where +.Va extension +is the typical file name extension used on files +compressed such that +.Nm sh Fl c Li ' Ns Va command Ns Li ' +decompresses the files. +This variable is ignored when running as root. +.It Ev MANPAGER +The pager to used. By default +.Xr less 1 +is used. +.It Ev PAGER +Used instead of +.Ev MANPAGER +if +.Ev MANPAGER +is not set. +.It Ev MANWIDTH +The display-width of the manual, in columns. +.It Ev COLUMNS +Used instead of +.Ev MANWIDTH +if +.Ev MANWIDTH +is not set. +.It Ev MANMAXWIDTH +The maximum display-width of the manual, in columns. +.It Ev MAN_KEEP_STDERR +If set and not empty, +.Nm +will not redirect stderr to +.Pa /dev/null +before parsing the file and before starting the pager. +.It Ev MAN_KEEP_FORMATTING +If set and not empty, +.Nm +will not remove formatting information from the output. +Formatting is always keeped when stdout is a terminal +(which causes +.Nm +to start a pager). +.Ed +.Sh SEE ALSO +.Xr less 1 +.Sh STANDARDS +The +.Nm +utility is compliant with the +.St -p1003.1-2013 +specification except from the +.Op Fl k +flag and support for looking up multiple manuals. +.Pp +The +.Op Fl 78 +flags, +.Ar section +and all environment variables except +.Ev PAGER +are an extension to that specification. diff --git a/man.c b/man.c new file mode 100644 index 0000000..0e02aea --- /dev/null +++ b/man.c _AT_@ -0,0 +1,677 @@ +/* See LICENSE file for copyright and license details. */ +#include <sys/ioctl.h> +#include <sys/stat.h> +#include <sys/wait.h> + +#include <dirent.h> +#include <errno.h> +#include <fcntl.h> +#include <signal.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "config.h" +#include "util.h" + +static void +usage(void) +{ + eprintf("usage: %s [-7 | -8] [section] name [subcommand ...]\n", argv0); +} + +static void +backslash(char *out, const char *str, const char *esc) +{ + while (*str) { + if (strchr(esc, *str)) + *out++ = '\\'; + *out++ = *str++; + } + *out = '\0'; +} + +static void +setprompt(const char *name, const char *sect) +{ + char *buf1, *buf2; + const char *p; + size_t n; + +#define PATTERN "-ix8RmPm Manual page %s ?ltline %%lt?L/%%L.:byte %%bB?s/%%s..?e (END):?pB %%pB\\%%.. (press h for help or q to quit)$PM Manual page %s ?ltline %%lt?L/%%L.:byte %%bB?s/%%s..?e (END):?pB %%pB\\%%.. (press h for help or q to quit)$" + + if ((p = strchr(name, '/'))) + name = p + 1; + + n = strlen(name) + (sect ? strlen(sect) : 0) + 3; + buf1 = emalloc(n + sizeof(PATTERN)); + buf2 = emalloc(n); + + if (sect) + sprintf(buf1, "%s(%s)", name, sect); + else + sprintf(buf1, "%s", name); + if (setenv("MAN_PN", buf1, 1)) + weprintf("setenv MAN_PN:"); + + backslash(buf2, buf1, "$?:.%\\"); + sprintf(buf1, PATTERN, buf2, buf2); + if (setenv("LESS", buf1, 1)) + weprintf("setenv LESS:"); + + free(buf1); + free(buf2); + +#undef PATTERN +} + +static void +checkmap(const char *s, const char *name) +{ + char last = ':'; + int need_eq = 0; + for (; *s; last = *s++) { + if (*s == ':') { + if (need_eq || last == '=') + goto bad; + need_eq = 1; + } else if (*s == '=') { + need_eq = 0; + if (last == ':') + goto bad; + } + } + return; +bad: + eprintf("%s is malformatted\n", name); +} + +static void +xdup2(int old, int new) +{ + if (old != new) { + if (dup2(old, new) == -1) + eprintf("dup2:"); + close(old); + } +} + +static void +xwaitpid(pid_t pid) +{ + int status; + if (pid < 0) + return; + if (waitpid(pid, &status, 0) != pid) + eprintf("waitpid:"); + if (status) + exit(1); +} + +static char * +xgetenv(const char *var, const char *def, int secure) +{ + const char *env = NULL; + if (secure || getuid()) + env = getenv(var); + return estrdup(env ? env : def); +} + +static char * +xstrchr(char *s, int c) +{ + char *r = strchr(s, c); + return r ? r : strchr(s, '\0'); +} + +static int +hasextension(const char *path, const char *ext) +{ + char *p; + if (strlen(path) <= strlen(ext)) + return 0; + p = strchr(path, '\0'); + p -= strlen(ext); + return p[-1] == '.' && !strcmp(p, ext); +} + +static int +find_man_in_dir(const char *dir, const char *name, const char *sectprefix, const char *ext) +{ + DIR *d; + struct dirent *f; + char *p, *sect; + int dirfd, fd; + size_t n; + + dirfd = open(dir, O_DIRECTORY); + if (dirfd < 0) + return -1; + d = fdopendir(dirfd); + if (!d) + return -1; + + while ((f = readdir(d))) { + p = f->d_name; + n = strlen(name); + if (strncmp(p, name, n)) + continue; + p += n; + if (*p++ != '.') + continue; + n = strlen(sectprefix); + if (strncmp(p, sectprefix, n)) + continue; + sect = p; + p = strchr(p + n, '.'); + if (p) + *p = '\0'; + if (ext ? (p && !strcmp(p + 1, ext)) : !p) { + setprompt(name, sect); + if (p) + *p = '.'; + fd = openat(dirfd, f->d_name, O_RDONLY); + if (fd >= 0) { + closedir(d); + close(dirfd); + return fd; + } + } + } + + closedir(d); + close(dirfd); + return -1; +} + +static int +cleanabspath(char *path, size_t size, const char *cwd) +{ + char *p, *q; + size_t i, n, len; + char *parts[PATH_MAX]; + + if (*path != '/') { + if (strlen(path) + strlen(cwd) + 2 > size) + return -1; + memmove(path + strlen(cwd) + 1, path, strlen(path) + 1); + *stpcpy(path, cwd) = '/'; + } + + for (n = 0, p = q = path; *p; p++) { + if (*p == '/') { + *p = '\0'; + parts[n++] = q; + q = p + 1; + } + } + + for (i = 0; i < n;) { + if (!parts[i][parts[i][0] == '.']) { + memmove(parts + i, parts + i + 1, --n - i); + } else if (parts[i][0] == '.' && parts[i][1] == '.' && !parts[i][2]) { + memmove(parts + i, parts + i + 1, --n - i); + if (i) { + i--; + memmove(parts + i, parts + i + 1, --n - i); + } + } else { + i++; + } + } + + for (p = path, i = 0; i < n; i++) { + *p++ = '/'; + len = strlen(parts[i]); + memmove(p, parts[i], len); + p += len; + } + *p = '\0'; + if (!*path) + path[0] = '/', path[1] = '\0'; + + return 0; +} + +static char * +getmanpath(const char *name, char *manpathtrans) +{ + char *p, *q, *t, *ret, cwd[PATH_MAX], path[PATH_MAX], clean[PATH_MAX]; + int done = 0; + + if (strlen(name) >= sizeof(path)) + return NULL; + + if (!getcwd(cwd, sizeof(cwd))) + eprintf("getcwd:"); + + strcpy(path, name); + p = strrchr(path, '/'); + *p = '\0'; + p = strrchr(path, '/'); + if (p && p != path) + *p = '\0'; + if (cleanabspath(path, sizeof(path), cwd)) + return NULL; + + for (p = manpathtrans; !done; p = q + 1) { + q = xstrchr(p, ':'); + done = !*q; + *q = '\0'; + t = strchr(p, '='); + if (!t) + break; + *t++ = '\0'; + + if (strlen(p) >= sizeof(clean)) + continue; + strcpy(clean, p); + if (cleanabspath(clean, sizeof(clean), cwd)) + continue; + + if (strcmp(path, clean)) + continue; + + ret = emalloc(strlen(t) + sizeof("/share/man")); + stpcpy(stpcpy(ret, t), "/share/man"); + return ret; + } + + ret = emalloc(strlen(path) + sizeof("/share/man")); + stpcpy(stpcpy(ret, path), "/share/man"); + return ret; +} + +static int +openman(const char *name, char *manpath, char *manpathtrans, char *mansect, + char *manbinsect, char *manetcsect, char *mancomp, char *pathbuf, pid_t *pid) +{ + char *pp = NULL, *ps = NULL, *pc, *qp, *qs, *qc, cp, cs, cc, *p; + char *temp_manpath = NULL; + const char *orig_name = NULL; + int fd, rw[2]; + struct stat st; + + *pid = -1; + +#define FOR(s, p, q, c)\ + for (p = s, c = 1;\ + c ? (q = xstrchr(p, ':'), c = *q, *q = '\0', 1) : 0;\ + *q = c, p = q + 1) + + if (strchr(name, '/')) { + if (!stat(name, &st) && S_ISREG(st.st_mode) && (st.st_mode & 0111)) { + manpath = temp_manpath = getmanpath(name, manpathtrans); + if (!manpath) + return -1; + mansect = manbinsect; + name = strrchr(name, '/') + 1; + goto search; + } + + /* This part is a just a convenience, nothing in the POSIX + * standard suggests that this feature many be available. */ + if (strstr(name, "/etc/") == name) { + orig_name = name; + name = strrchr(name, '/') + 1; + mansect = manetcsect; + goto search; + } + + try_file: + strcpy(pathbuf, name); + fd = open(pathbuf, O_RDONLY); + if (fd < 0) + eprintf("open %s:", pathbuf); + setprompt(pathbuf, NULL); + FOR(mancomp, pc, qc, cc) { + *(p = strchr(pc, '=')) = '\0'; + if (hasextension(name, pc)) { + *p = '='; + goto match; + } + *p = '='; + } + pc = NULL; + goto match; + } + +#define SEARCH\ + FOR(mansect, ps, qs, cs) {\ + if (!*ps)\ + continue;\ + FOR(manpath, pp, qp, cp) {\ + sprintf(pathbuf, "%s/man%c/%s.%s", pp, *ps, name, ps);\ + if (pc) {\ + *(p = strchr(pc, '=')) = '\0';\ + stpcpy(stpcpy(strchr(pathbuf, '\0'), "."), pc);\ + *p = '=';\ + }\ + fd = open(pathbuf, O_RDONLY);\ + if (fd >= 0) {\ + setprompt(name, ps);\ + goto match;\ + }\ + if (!ps[1]) {\ + sprintf(pathbuf, "%s/man%c", pp, *ps);\ + if (pc)\ + *(p = strchr(pc, '=')) = '\0';\ + fd = find_man_in_dir(pathbuf, name, ps, pc);\ + if (pc)\ + *p = '=';\ + if (fd >= 0)\ + goto match;\ + }\ + }\ + } + +search: + FOR(mancomp, pc, qc, cc) + SEARCH + pc = NULL; + SEARCH + +#undef SEARCH +#undef FOR + + free(temp_manpath); + if (orig_name) { + name = orig_name; + goto try_file; + } + return -1; +match: + + free(temp_manpath); + if (!pc) + return fd; + pc = strchr(pc, '=') + 1; + + if (pipe(rw)) + eprintf("pipe:"); + + switch ((*pid = fork())) { + case -1: + eprintf("fork:"); + case 0: + close(rw[0]); + xdup2(fd, STDIN_FILENO); + xdup2(rw[1], STDOUT_FILENO); + execl("/bin/sh", "sh", "-c", pc, NULL); + eprintf("execl /bin/sh:"); + default: + close(rw[1]); + close(fd); + break; + } + + return rw[0]; +} + +static int +convman(int fd, size_t columns, int ascii, int null_stderr, pid_t *pid) +{ + char arg1[3 * sizeof(size_t) + sizeof("-rLL=n")]; + char arg2[3 * sizeof(size_t) + sizeof("-rLT=n")]; + int rw[2]; + + if (pipe(rw)) + eprintf("pipe:"); + + switch ((*pid = fork())) { + case -1: + eprintf("fork:"); + case 0: + close(rw[0]); + xdup2(fd, STDIN_FILENO); + xdup2(rw[1], STDOUT_FILENO); + if (null_stderr) { + fd = open("/dev/null", O_WRONLY); + if (fd != STDERR_FILENO) { + dup2(fd, STDERR_FILENO); + close(fd); + } + } + sprintf(arg1, "-rLL=%zun", columns); + sprintf(arg2, "-rLT=%zun", columns); + execv(GROFF, (char *[]){GROFF, arg1, arg2, "-mtty-char", "-mandoc", + ascii ? "-Tascii" : "-Tutf8", NULL}); + eprintf("exec %s:", GROFF); + default: + close(rw[1]); + break; + } + + return rw[0]; +} + +static size_t +remove_lfs_head(char *str, size_t n) +{ + size_t off; + char *p; + for (p = str; n && *p == '\n'; p++, n--); + if (p != str) + memmove(str, p, n); + p = memchr(str, '\n', n); + if (!p) + return n; + if (*p == '\n') + p++; + n -= off = (size_t)(++p - str); + while (n && *p == '\n') + n--, p++; + memmove(str + off, p, n); + return off + n; +} + +static size_t +remove_lfs_foot(char *str, size_t n) +{ + char *p, *q; + p = str + n; + if (n && p[-1] == '\n') { + while (p != str && p[-1] == '\n') + p--, n--; + n++; + } + while (p != str && p[-1] != '\n') + p--; + if (p-- == str) + return n; + if (p == str || p[-1] != '\n') + return n; + q = --p; + while (p != str && p[-1] == '\n') + p--; + memmove(p, q, n - (size_t)(q - str)); + return n - (size_t)(q - p); +} + +static size_t +rmfmt(char *str, size_t n) +{ + char *r, *w; + for (r = w = str; n--; r++) { + if (*r == '\b') { + while (w != str && (w[-1] & 0xC0) == 0x80) + w--; + w -= w != str; + } else { + *w++ = *r; + } + } + return (size_t)(w - str); +} + +int +main(int argc, char *argv[]) +{ + char *manpathtrans = xgetenv("MANPATHTRANS", MANPATHTRANS, 1); + char *manpath = xgetenv("MANPATH", MANPATH, 1); + char *mansect = xgetenv("MANSECT", MANSECT, 1); + char *manbinsect = xgetenv("MANBINSECT", MANBINSECT, 1); + char *manetcsect = xgetenv("MANETCSECT", MANETCSECT, 1); + char *mancomp = xgetenv("MANCOMP", MANCOMP, 0); + const char *pager = NULL, *env; + char *name, *p, *buf; + size_t len, ptr, siz, max, columns = 0; + int i, rfd = -1, cfd, fd, rw[2]; + int ascii = 0; + int null_stderr; + pid_t zpid, ppid; + struct winsize winsize; + ssize_t r; + + if ((env = getenv("TERM"))) + ascii = !strcmp(env, "linux"); + + ARGBEGIN { + case '7': + ascii = 1; + break; + case '8': + ascii = 0; + break; + default: + usage(); + } ARGEND + if (!argc) + usage(); + + checkmap(manpathtrans, "MANPATHTRANS"); + checkmap(mancomp, "MANCOMP"); + + if (isatty(STDOUT_FILENO)) { + if (getuid()) { + pager = getenv("MANPAGER"); + pager = pager ? pager : getenv("PAGER"); + } + pager = pager ? pager : MANPAGER; + } + + len = 0; + for (i = 0; i < argc; i++) + len += strlen(argv[i]) + 1; + p = name = emalloc(len); + p = stpcpy(p, argv[0]); + for (i = 1; i < argc; i++) + *p++ = '-', p = stpcpy(p, argv[i]); + *p = '\0'; + + buf = emalloc(strlen(name) + strlen(manpath) + + strlen(mansect) + strlen(mancomp) + 9); + if (argc > 1 && !strchr(argv[0], '/')) { + p = name + strlen(argv[0]); + *p = '\0'; + rfd = openman(p + 1, manpath, manpathtrans, name, + name, name, mancomp, buf, &zpid); + *p = '-'; + if (rfd < 0 && strchr(p + 1, '/')) + eprintf("could not find a manual\n"); + } + if (rfd < 0) + rfd = openman(name, manpath, manpathtrans, mansect, + manbinsect, manetcsect, mancomp, buf, &zpid); + if (rfd < 0) + eprintf("could not find a manual\n"); + + free(name); + free(manpathtrans); + free(manpath); + free(mansect); + free(manbinsect); + free(manetcsect); + free(mancomp); + free(buf); + + if ((env = getenv("MANWIDTH"))) + columns = strtonum(env, 0, SIZE_MAX, NULL); + if (!columns && (env = getenv("COLUMNS"))) + columns = strtonum(env, 0, SIZE_MAX, NULL); + if (!columns) { + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &winsize) == -1) + columns = 80; + else + columns = winsize.ws_col; + } + if ((env = getenv("MANMAXWIDTH"))) { + max = strtonum(env, 0, SIZE_MAX, NULL); + columns = columns < max ? columns : max; + } + + null_stderr = (env = getenv("MAN_KEEP_STDERR"), !env || !*env); + + cfd = convman(rfd, columns, ascii, null_stderr, &ppid); + close(rfd); + + buf = emalloc(siz = BUFSIZ); + for (len = 0;;) { + if (len == siz) + buf = erealloc(buf, siz <<= 1); + r = read(cfd, buf + len, siz - len); + if (r < 0) + eprintf("read <subprocess>:"); + if (r == 0) + break; + len += (size_t)r; + } + + len = remove_lfs_head(buf, len); + len = remove_lfs_foot(buf, len); + if (!pager && (env = getenv("MAN_KEEP_FORMATTING"), !env || !*env)) + len = rmfmt(buf, len); + + close(cfd); + xwaitpid(zpid); + xwaitpid(ppid); + + if (null_stderr) { + fd = open("/dev/null", O_WRONLY); + if (fd != STDERR_FILENO) { + dup2(fd, STDERR_FILENO); + close(fd); + } + } + + ppid = -1; + if (pager) { + if (pipe(rw)) + eprintf("pipe:"); + switch ((ppid = fork())) { + case -1: + eprintf("fork:"); + case 0: + close(rw[1]); + xdup2(rw[0], STDIN_FILENO); + execl("/bin/sh", "sh", "-c", pager, NULL); + eprintf("execl /bin/sh:"); + default: + close(rw[0]); + xdup2(rw[1], STDOUT_FILENO); + close(STDIN_FILENO); + break; + } + } + + if (signal(SIGPIPE, SIG_IGN) == SIG_ERR) + weprintf("signal SIGPIPE:"); + + for (ptr = 0; ptr < len;) { + r = write(STDOUT_FILENO, buf + ptr, len - ptr); + if (r < 0) { + if (errno == EPIPE) + break; + eprintf("write %s:", pager ? "<pager>" : "<stdout>"); + } + ptr += (size_t)r; + } + free(buf); + close(STDOUT_FILENO); + + xwaitpid(ppid); + + return 0; +} -- 2.11.0Received on Wed Feb 01 2017 - 10:28:06 CET
This archive was generated by hypermail 2.3.0 : Wed Feb 01 2017 - 10:36:21 CET