1/* See LICENSE file for copyright and license details. */
2
3#include <limits.h>
4#include <stdint.h>
5#include <stdio.h>
6#include <stdlib.h>
7#include <string.h>
8
9#include "text.h"
10#include "utf.h"
11#include "util.h"
12
13static size_t startnum = 1;
14static size_t incr = 1;
15static size_t blines = 1;
16static size_t delimlen = 2;
17static size_t seplen = 1;
18static int width = 6;
19static int pflag = 0;
20static char type[] = {'n', 't', 'n'}; /* footer, body, header */
21static char *delim = "\\:";
22static char format[6] = "%*ld";
23static char *sep = "\t";
24static regex_t preg[3];
25
26static int
27getsection(struct line *l, int *section)
28{
29 size_t i;
30 int sectionchanged = 0, newsection = *section;
31
32 for (i = 0; (l->len - i) >= delimlen && !memcmp(l->data + i, delim, delimlen); i += delimlen) {
33 if (!sectionchanged) {
34 sectionchanged = 1;
35 newsection = 0;
36 } else {
37 newsection = (newsection + 1) % 3;
38 }
39 }
40
41 if (!(l->len - i) || l->data[i] == '\n')
42 *section = newsection;
43 else
44 sectionchanged = 0;
45
46 return sectionchanged;
47}
48
49static void
50nl(const char *fname, FILE *fp)
51{
52 static struct line line;
53 static size_t size;
54 size_t number = startnum, bl = 1;
55 ssize_t len;
56 int donumber, oldsection, section = 1;
57
58 while ((len = getline(&line.data, &size, fp)) > 0) {
59 line.len = len;
60 donumber = 0;
61 oldsection = section;
62
63 if (getsection(&line, §ion)) {
64 if ((section >= oldsection) && !pflag)
65 number = startnum;
66 continue;
67 }
68
69 switch (type[section]) {
70 case 't':
71 if (line.data[0] != '\n')
72 donumber = 1;
73 break;
74 // ?man -p: preserve file attributes
75 case 'p':
76 if (!regexec(preg + section, line.data, 0, NULL, 0))
77 donumber = 1;
78 break;
79 case 'a':
80 if (line.data[0] == '\n' && bl < blines) {
81 ++bl;
82 } else {
83 donumber = 1;
84 bl = 1;
85 }
86 }
87
88 if (donumber) {
89 printf(format, width, number);
90 fwrite(sep, 1, seplen, stdout);
91 number += incr;
92 }
93 fwrite(line.data, 1, line.len, stdout);
94 }
95 free(line.data);
96 if (ferror(fp))
97 eprintf("getline %s:", fname);
98}
99
100static void
101usage(void)
102{
103 eprintf(
104 "usage: %s [-p] [-b type] [-d delim] [-f type]\n"
105 " [-h type] [-i num] [-l num] [-n format]\n"
106 " [-s sep] [-v num] [-w num] [file]\n",
107 argv0
108 );
109}
110
111static char
112getlinetype(char *type, regex_t *preg)
113{
114 if (type[0] == 'p')
115 eregcomp(preg, type + 1, REG_NOSUB);
116 else if (!type[0] || !strchr("ant", type[0]))
117 usage();
118
119 return type[0];
120}
121
122// ?man nl: number lines
123// ?man number the lines of files
124int
125main(int argc, char *argv[])
126{
127 FILE *fp = NULL;
128 size_t s;
129 int ret = 0;
130 char *d, *formattype, *formatblit;
131
132 ARGBEGIN
133 {
134 // ?man -d:str: specify directory
135 case 'd':
136 switch (utflen((d = EARGF(usage())))) {
137 case 0:
138 eprintf("empty logical page delimiter\n");
139 break;
140 case 1:
141 s = strlen(d);
142 delim = emalloc(s + 1 + 1);
143 estrlcpy(delim, d, s + 1 + 1);
144 estrlcat(delim, ":", s + 1 + 1);
145 delimlen = s + 1;
146 break;
147 default:
148 delim = d;
149 delimlen = strlen(delim);
150 break;
151 }
152 break;
153 // ?man -f:str: force the operation
154 case 'f':
155 type[0] = getlinetype(EARGF(usage()), preg);
156 break;
157 // ?man -b:str: specify block size or base directory
158 case 'b':
159 type[1] = getlinetype(EARGF(usage()), preg + 1);
160 break;
161 // ?man -h:str: suppress headers or print help
162 case 'h':
163 type[2] = getlinetype(EARGF(usage()), preg + 2);
164 break;
165 // ?man -i:num: interactive mode or prompt for confirmation
166 case 'i':
167 incr = estrtonum(
168 EARGF(usage()), 0, MIN((unsigned long long)LLONG_MAX, (unsigned long long)SIZE_MAX)
169 );
170 break;
171 // ?man -l:num: list in long format
172 case 'l':
173 blines = estrtonum(
174 EARGF(usage()), 0, MIN((unsigned long long)LLONG_MAX, (unsigned long long)SIZE_MAX)
175 );
176 break;
177 // ?man -n:str: print line numbers or counts
178 case 'n':
179 formattype = EARGF(usage());
180 estrlcpy(format, "%", sizeof(format));
181
182 if (!strcmp(formattype, "ln")) {
183 formatblit = "-";
184 } else if (!strcmp(formattype, "rn")) {
185 formatblit = "";
186 } else if (!strcmp(formattype, "rz")) {
187 formatblit = "0";
188 } else {
189 eprintf("%s: bad format\n", formattype);
190 }
191
192 estrlcat(format, formatblit, sizeof(format));
193 estrlcat(format, "*ld", sizeof(format));
194 break;
195 // ?man -p: preserve file attributes
196 case 'p':
197 pflag = 1;
198 break;
199 // ?man -s:str: silent mode or print summary
200 case 's':
201 sep = EARGF(usage());
202 seplen = unescape(sep);
203 break;
204 // ?man -v:num: verbose mode; show progress
205 case 'v':
206 startnum = estrtonum(
207 EARGF(usage()), 0, MIN((unsigned long long)LLONG_MAX, (unsigned long long)SIZE_MAX)
208 );
209 break;
210 // ?man -w:num: wait for completion
211 case 'w':
212 width = estrtonum(EARGF(usage()), 1, INT_MAX);
213 break;
214 default:
215 usage();
216 }
217 ARGEND
218
219 if (argc > 1)
220 usage();
221
222 if (!argc) {
223 nl("<stdin>", stdin);
224 } else {
225 if (!strcmp(argv[0], "-")) {
226 argv[0] = "<stdin>";
227 fp = stdin;
228 } else if (!(fp = fopen(argv[0], "r"))) {
229 eprintf("fopen %s:", argv[0]);
230 }
231 nl(argv[0], fp);
232 }
233
234 ret |= fp && fp != stdin && fshut(fp, argv[0]);
235 ret |= fshut(stdin, "<stdin>") | fshut(stdout, "<stdout>");
236
237 return ret;
238}