1 /* man.c - Read system documentation
2  *
3  * Copyright 2019 makepost <makepost@firemail.cc>
4  *
5  * See http://pubs.opengroup.org/onlinepubs/9699919799/utilities/man.html
6 
7 USE_MAN(NEWTOY(man, "k:M:", TOYFLAG_USR|TOYFLAG_BIN))
8 
9 config MAN
10   bool "man"
11   default n
12   help
13     usage: man [-M PATH] [-k STRING] | [SECTION] COMMAND
14 
15     Read manual page for system command.
16 
17     -k	List pages with STRING in their short description
18     -M	Override $MANPATH
19 
20     Man pages are divided into 8 sections:
21     1 commands      2 system calls  3 library functions  4 /dev files
22     5 file formats  6 games         7 miscellaneous      8 system management
23 
24     Sections are searched in the order 1 8 3 2 5 4 6 7 unless you specify a
25     section. Each section has a page called "intro", and there's a global
26     introduction under "man-pages".
27 */
28 
29 #define FOR_man
30 #include <toys.h>
31 
32 GLOBALS(
33   char *M, *k;
34 
35   char any, cell, ex, *f, k_done, *line, *m, **sct, **scts, **sufs;
36   regex_t reg;
37 )
38 
newln()39 static void newln()
40 {
41   if (FLAG(k)) return;
42   if (TT.any) putchar('\n');
43   if (TT.any && TT.cell != 2) putchar('\n'); // gawk alias
44   TT.any = TT.cell = 0;
45 }
46 
put(char * x)47 static void put(char *x)
48 {
49   while (*x && (TT.ex || *x != '\n')) TT.any = putchar(*x++);
50 }
51 
52 // Substitute with same length or shorter.
s(char * x,char * y)53 static void s(char *x, char *y)
54 {
55   int i = strlen(x), j = strlen(y), k, l;
56 
57   for (k = 0; TT.line[k]; k++) if (!strncmp(x, &TT.line[k], i)) {
58     memmove(&TT.line[k], y, j);
59     for (l = k += j; TT.line[l]; l++) TT.line[l] = TT.line[l + i - j];
60     k--;
61   }
62 }
63 
start(char * x)64 static char start(char *x)
65 {
66   return !strncmp(x, TT.line, strlen(x));
67 }
68 
trim(char * x)69 static void trim(char *x)
70 {
71   if (start(x)) while (*x++) TT.line++;
72 }
73 
k(char * s)74 static char k(char *s) {
75   TT.k_done = 2;
76   if (s) TT.line = s;
77   return !regexec(&TT.reg, TT.k, 0, 0, 0)||!regexec(&TT.reg, TT.line, 0, 0, 0);
78 }
79 
do_man(char ** pline,long len)80 static void do_man(char **pline, long len)
81 {
82   if (!pline) return newln();
83   TT.line = *pline;
84 
85   if (FLAG(k)) {
86     if (!TT.k_done && !start(".") && !start("'") && k(strstr(*pline, "- ")))
87       printf("%-20s %s%s", TT.k, "- "+2*(TT.line!=*pline), TT.line);
88     else if (!TT.k_done && start(".so") && k(basename(*pline + 4)))
89       printf("%s - See %s", TT.k, TT.line);
90   } else {
91     s("\\fB", ""), s("\\fI", ""), s("\\fP", ""), s("\\fR", ""); // bash bold,ita
92     s("\\(aq", "'"), s("\\(cq", "'"), s("\\(dq", "\""); // bash,rsync quote
93     s("\\*(lq", "\""), s("\\*(rq", "\""); // gawk quote
94     s("\\(bu", "*"), s("\\(bv", "|"); // bash symbol
95     s("\\&", ""), s("\\f(CW", ""); // gawk,rsync fancy
96     s("\\-", "-"), s("\\(", ""), s("\\^", ""), s("\\e", "\\"); // bash escape
97     s("\\*(", "#"); // gawk var
98 
99     if (start(".BR")) trim(".BR "), s(" ", ""); // bash boldpunct
100     if (start(".IP")) newln(), trim(".IP "); // bash list
101     if (start(".IR")) trim(".IR "), s(" ", ""); // bash itapunct
102 
103     trim(".B "); // bash bold
104     trim(".BI "); // gawk boldita
105     trim(".FN "); // bash filename
106     trim(".I "); // bash ita
107     trim(".if n "); // bash nroff
108     if (start(".E")) TT.ex = TT.line[2] == 'X'; // stat example
109     else if (start(".PP")) newln(); // bash paragraph
110     else if (start(".SM")); // bash small
111     else if (start(".S")) newln(), put(TT.line + 4), newln(); // bash section
112     else if (start(".so")) put("See "), put(basename(TT.line + 4)); // lastb
113     else if (start(".TH")) s("\"", " "), put(TT.line + 4); // gawk,git head
114     else if (start(".TP")) newln(), TT.cell = 1; // bash table
115     else if (start(".") || start("\'")); // bash,git garbage
116     else if (!*TT.line); // emerge
117     else {
118       if (TT.cell) TT.cell++;
119       if (!TT.ex) put(" ");
120       put(TT.line);
121     }
122   }
123 }
124 
125 // Open file, decompressing if suffix known.
zopen(char * s)126 static int zopen(char *s)
127 {
128   int fds[] = {-1, -1};
129   char **known = TT.sufs, *suf = strrchr(s, '.');
130 
131   if ((*fds = open(s, O_RDONLY)) == -1) return -1;
132   while (suf && *known && strcmp(suf, *known++));
133   if (!suf || !*known) return *fds;
134   sprintf(toybuf, "%czcat"+2*(suf[1]=='g'), suf[1]);
135   xpopen_both((char *[]){toybuf, s, 0}, fds);
136   close(fds[0]);
137   return fds[1];
138 }
139 
manpath()140 static char manpath()
141 {
142   if (*++TT.sct) return 0;
143   if (!(TT.m = strsep(&TT.M, ":"))) return 1;
144   TT.sct = TT.scts;
145   return 0;
146 }
147 
148 // Try opening all the possible file extensions.
tryfile(char * name)149 static int tryfile(char *name)
150 {
151   int dotnum, fd = -1;
152   char *s = xmprintf("%s/man%s/%s.%s.bz2", TT.m, *TT.sct, name, *TT.sct), **suf;
153   size_t len = strlen(s) - 4;
154 
155   for (dotnum = 0; dotnum <= 2; dotnum += 2) {
156     suf = TT.sufs;
157     while ((fd == -1) && *suf) strcpy(s + len - dotnum, *suf++), fd = zopen(s);
158     // Recheck suf in zopen, because for x.1.gz name here it is "".
159   }
160   free(s);
161   return fd;
162 }
163 
man_main(void)164 void man_main(void)
165 {
166   int fd = -1;
167   TT.scts = (char *[]) {"1", "8", "3", "2", "5", "4", "6", "7", 0};
168   TT.sct = TT.scts - 1; // First manpath() read increments.
169   TT.sufs = (char *[]) {".bz2", ".gz", ".xz", "", 0};
170 
171   if (!TT.M) TT.M = getenv("MANPATH");
172   if (!TT.M) TT.M = "/usr/share/man";
173 
174   if (FLAG(k)) {
175     char *d, *f;
176     DIR *dp;
177     struct dirent *entry;
178 
179     xregcomp(&TT.reg, TT.k, REG_ICASE|REG_NOSUB);
180     while (!manpath()) {
181       d = xmprintf("%s/man%s", TT.m, *TT.sct);
182       if (!(dp = opendir(d))) continue;
183       while ((entry = readdir(dp))) {
184         if (entry->d_name[0] == '.') continue;
185         f = xmprintf("%s/%s", d, TT.k = entry->d_name);
186         if (-1 != (fd = zopen(f))) {
187           TT.k_done = 0;
188           do_lines(fd, '\n', do_man);
189         }
190         free(f);
191       }
192       closedir(dp);
193       free(d);
194     }
195     return regfree(&TT.reg);
196   }
197 
198   if (!toys.optc) help_exit("which page?");
199 
200   if (toys.optc == 1) {
201     if (strchr(*toys.optargs, '/')) fd = zopen(*toys.optargs);
202     else while ((fd == -1) && !manpath()) fd = tryfile(*toys.optargs);
203     if (fd == -1) error_exit("no %s", *toys.optargs);
204 
205   // If they specified a section, look for file in that section
206   } else {
207     TT.scts = (char *[]){*toys.optargs, 0}, TT.sct = TT.scts - 1;
208     while ((fd == -1) && !manpath()) fd = tryfile(toys.optargs[1]);
209     if (fd == -1) error_exit("section %s no %s", *--TT.sct, toys.optargs[1]);
210   }
211 
212   do_lines(fd, '\n', do_man);
213 }
214