1*b7041c07Sderaadt /* $OpenBSD: sdiff.c,v 1.39 2021/10/24 21:24:17 deraadt Exp $ */
2228e906dStedu
3228e906dStedu /*
4228e906dStedu * Written by Raymond Lai <ray@cyth.net>.
5228e906dStedu * Public domain.
6228e906dStedu */
7228e906dStedu
8228e906dStedu #include <sys/queue.h>
91e22bbb6Sotto #include <sys/stat.h>
10228e906dStedu #include <sys/types.h>
11228e906dStedu #include <sys/wait.h>
12228e906dStedu
13228e906dStedu #include <ctype.h>
14228e906dStedu #include <err.h>
151e22bbb6Sotto #include <errno.h>
161e22bbb6Sotto #include <fcntl.h>
17228e906dStedu #include <getopt.h>
18228e906dStedu #include <limits.h>
1931342e88Sray #include <paths.h>
201357284aSmillert #include <stdint.h>
21228e906dStedu #include <stdio.h>
22228e906dStedu #include <stdlib.h>
23228e906dStedu #include <string.h>
24228e906dStedu #include <unistd.h>
25228e906dStedu #include <util.h>
26228e906dStedu
271e22bbb6Sotto #include "common.h"
28228e906dStedu #include "extern.h"
29228e906dStedu
30228e906dStedu #define WIDTH 130
31228e906dStedu /*
32228e906dStedu * Each column must be at least one character wide, plus three
33228e906dStedu * characters between the columns (space, [<|>], space).
34228e906dStedu */
35228e906dStedu #define WIDTH_MIN 5
36228e906dStedu
37228e906dStedu /* A single diff line. */
38228e906dStedu struct diffline {
39228e906dStedu SIMPLEQ_ENTRY(diffline) diffentries;
40951d632eSotto char *left;
41228e906dStedu char div;
42951d632eSotto char *right;
43228e906dStedu };
44228e906dStedu
45228e906dStedu static void astrcat(char **, const char *);
46951d632eSotto static void enqueue(char *, char, char *);
471e22bbb6Sotto static char *mktmpcpy(const char *);
48951d632eSotto static void freediff(struct diffline *);
49228e906dStedu static void int_usage(void);
5049e0b549Sotto static int parsecmd(FILE *, FILE *, FILE *);
51228e906dStedu static void printa(FILE *, size_t);
52228e906dStedu static void printc(FILE *, size_t, FILE *, size_t);
53228e906dStedu static void printcol(const char *, size_t *, const size_t);
5449e0b549Sotto static void printd(FILE *, size_t);
55228e906dStedu static void println(const char *, const char, const char *);
56228e906dStedu static void processq(void);
57228e906dStedu static void prompt(const char *, const char *);
58228e906dStedu __dead static void usage(void);
59228e906dStedu static char *xfgets(FILE *);
60228e906dStedu
61228e906dStedu SIMPLEQ_HEAD(, diffline) diffhead = SIMPLEQ_HEAD_INITIALIZER(diffhead);
62228e906dStedu size_t line_width; /* width of a line (two columns and divider) */
63228e906dStedu size_t width; /* width of each column */
64228e906dStedu size_t file1ln, file2ln; /* line number of file1 and file2 */
6549e0b549Sotto int Iflag = 0; /* ignore sets matching regexp */
66228e906dStedu int lflag; /* print only left column for identical lines */
67228e906dStedu int sflag; /* skip identical lines */
68d4ba4722Sray FILE *outfp; /* file to save changes to */
6931342e88Sray const char *tmpdir; /* TMPDIR or /tmp */
70228e906dStedu
71228e906dStedu static struct option longopts[] = {
72228e906dStedu { "text", no_argument, NULL, 'a' },
73228e906dStedu { "ignore-blank-lines", no_argument, NULL, 'B' },
74228e906dStedu { "ignore-space-change", no_argument, NULL, 'b' },
75228e906dStedu { "minimal", no_argument, NULL, 'd' },
76228e906dStedu { "ignore-tab-expansion", no_argument, NULL, 'E' },
77228e906dStedu { "diff-program", required_argument, NULL, 'F' },
78228e906dStedu { "speed-large-files", no_argument, NULL, 'H' },
79228e906dStedu { "ignore-matching-lines", required_argument, NULL, 'I' },
80ece621c6Sray { "ignore-case", no_argument, NULL, 'i' },
81228e906dStedu { "left-column", no_argument, NULL, 'l' },
82228e906dStedu { "output", required_argument, NULL, 'o' },
83228e906dStedu { "strip-trailing-cr", no_argument, NULL, 'S' },
84228e906dStedu { "suppress-common-lines", no_argument, NULL, 's' },
85228e906dStedu { "expand-tabs", no_argument, NULL, 't' },
86228e906dStedu { "ignore-all-space", no_argument, NULL, 'W' },
87228e906dStedu { "width", required_argument, NULL, 'w' },
88228e906dStedu { NULL, 0, NULL, 0 }
89228e906dStedu };
90228e906dStedu
911e22bbb6Sotto /*
921e22bbb6Sotto * Create temporary file if source_file is not a regular file.
931e22bbb6Sotto * Returns temporary file name if one was malloced, NULL if unnecessary.
941e22bbb6Sotto */
951e22bbb6Sotto static char *
mktmpcpy(const char * source_file)961e22bbb6Sotto mktmpcpy(const char *source_file)
971e22bbb6Sotto {
981e22bbb6Sotto struct stat sb;
991e22bbb6Sotto ssize_t rcount;
1001e22bbb6Sotto int ifd, ofd;
1011e22bbb6Sotto u_char buf[BUFSIZ];
1021e22bbb6Sotto char *target_file;
1031e22bbb6Sotto
1041e22bbb6Sotto /* Open input and output. */
105*b7041c07Sderaadt ifd = open(source_file, O_RDONLY);
1061e22bbb6Sotto /* File was opened successfully. */
1071e22bbb6Sotto if (ifd != -1) {
1081e22bbb6Sotto if (fstat(ifd, &sb) == -1)
1091e22bbb6Sotto err(2, "error getting file status from %s", source_file);
1101e22bbb6Sotto
1111e22bbb6Sotto /* Regular file. */
112675d97c0Sray if (S_ISREG(sb.st_mode)) {
113675d97c0Sray close(ifd);
1141e22bbb6Sotto return (NULL);
115675d97c0Sray }
1161e22bbb6Sotto } else {
1171e22bbb6Sotto /* If ``-'' does not exist the user meant stdin. */
1181e22bbb6Sotto if (errno == ENOENT && strcmp(source_file, "-") == 0)
1191e22bbb6Sotto ifd = STDIN_FILENO;
1201e22bbb6Sotto else
1211e22bbb6Sotto err(2, "error opening %s", source_file);
1221e22bbb6Sotto }
1231e22bbb6Sotto
1241e22bbb6Sotto /* Not a regular file, so copy input into temporary file. */
12531342e88Sray if (asprintf(&target_file, "%s/sdiff.XXXXXXXXXX", tmpdir) == -1)
12631342e88Sray err(2, "asprintf");
12731342e88Sray if ((ofd = mkstemp(target_file)) == -1) {
1281e22bbb6Sotto warn("error opening %s", target_file);
1291e22bbb6Sotto goto FAIL;
1301e22bbb6Sotto }
1311e22bbb6Sotto while ((rcount = read(ifd, buf, sizeof(buf))) != -1 &&
1321e22bbb6Sotto rcount != 0) {
1331e22bbb6Sotto ssize_t wcount;
1341e22bbb6Sotto
1351e22bbb6Sotto wcount = write(ofd, buf, (size_t)rcount);
1361e22bbb6Sotto if (-1 == wcount || rcount != wcount) {
1371e22bbb6Sotto warn("error writing to %s", target_file);
1381e22bbb6Sotto goto FAIL;
1391e22bbb6Sotto }
1401e22bbb6Sotto }
1411e22bbb6Sotto if (rcount == -1) {
1421e22bbb6Sotto warn("error reading from %s", source_file);
1431e22bbb6Sotto goto FAIL;
1441e22bbb6Sotto }
1451e22bbb6Sotto
1461e22bbb6Sotto close(ifd);
1471e22bbb6Sotto close(ofd);
1481e22bbb6Sotto
1491e22bbb6Sotto return (target_file);
1501e22bbb6Sotto
1511e22bbb6Sotto FAIL:
1521e22bbb6Sotto unlink(target_file);
1531e22bbb6Sotto exit(2);
1541e22bbb6Sotto }
1551e22bbb6Sotto
156228e906dStedu int
main(int argc,char ** argv)157228e906dStedu main(int argc, char **argv)
158228e906dStedu {
15949e0b549Sotto FILE *diffpipe, *file1, *file2;
1606bccf554Sderaadt size_t diffargc = 0, wflag = WIDTH;
161228e906dStedu int ch, fd[2], status;
162228e906dStedu pid_t pid;
163d4ba4722Sray const char *outfile = NULL;
1641e22bbb6Sotto char **diffargv, *diffprog = "diff", *filename1, *filename2,
1651e22bbb6Sotto *tmp1, *tmp2, *s1, *s2;
16632a0d318Smestre unsigned int Fflag = 0;
16762dc4a9eSderaadt
168228e906dStedu /*
169228e906dStedu * Process diff flags.
170228e906dStedu */
171228e906dStedu /*
172228e906dStedu * Allocate memory for diff arguments and NULL.
173228e906dStedu * Each flag has at most one argument, so doubling argc gives an
174228e906dStedu * upper limit of how many diff args can be passed. argv[0],
175228e906dStedu * file1, and file2 won't have arguments so doubling them will
176228e906dStedu * waste some memory; however we need an extra space for the
177228e906dStedu * NULL at the end, so it sort of works out.
178228e906dStedu */
1791ed98fdfSderaadt if (!(diffargv = calloc(argc, sizeof(char **) * 2)))
180a9ba38d6Stedu err(2, "main");
181228e906dStedu
182228e906dStedu /* Add first argument, the program name. */
183228e906dStedu diffargv[diffargc++] = diffprog;
184228e906dStedu
185618b0722Stedu while ((ch = getopt_long(argc, argv, "aBbdEHI:ilo:stWw:",
186228e906dStedu longopts, NULL)) != -1) {
187228e906dStedu const char *errstr;
188228e906dStedu
189228e906dStedu switch (ch) {
190228e906dStedu case 'a':
191228e906dStedu diffargv[diffargc++] = "-a";
192228e906dStedu break;
193228e906dStedu case 'B':
194228e906dStedu diffargv[diffargc++] = "-B";
195228e906dStedu break;
196228e906dStedu case 'b':
197228e906dStedu diffargv[diffargc++] = "-b";
198228e906dStedu break;
199228e906dStedu case 'd':
200228e906dStedu diffargv[diffargc++] = "-d";
201228e906dStedu break;
202228e906dStedu case 'E':
203228e906dStedu diffargv[diffargc++] = "-E";
204228e906dStedu break;
205228e906dStedu case 'F':
20646d91ad5Stedu diffargv[0] = diffprog = optarg;
20732a0d318Smestre Fflag = 1;
208228e906dStedu break;
209228e906dStedu case 'H':
210228e906dStedu diffargv[diffargc++] = "-H";
211228e906dStedu break;
212228e906dStedu case 'I':
21349e0b549Sotto Iflag = 1;
214228e906dStedu diffargv[diffargc++] = "-I";
215228e906dStedu diffargv[diffargc++] = optarg;
216228e906dStedu break;
217228e906dStedu case 'i':
218228e906dStedu diffargv[diffargc++] = "-i";
219228e906dStedu break;
220228e906dStedu case 'l':
221228e906dStedu lflag = 1;
222228e906dStedu break;
223228e906dStedu case 'o':
224d4ba4722Sray outfile = optarg;
225228e906dStedu break;
226228e906dStedu case 'S':
227228e906dStedu diffargv[diffargc++] = "--strip-trailing-cr";
228228e906dStedu break;
229228e906dStedu case 's':
230228e906dStedu sflag = 1;
231228e906dStedu break;
232228e906dStedu case 't':
233228e906dStedu diffargv[diffargc++] = "-t";
234228e906dStedu break;
235228e906dStedu case 'W':
236228e906dStedu diffargv[diffargc++] = "-w";
237228e906dStedu break;
238228e906dStedu case 'w':
239228e906dStedu wflag = strtonum(optarg, WIDTH_MIN,
240dc7cd303Stedu INT_MAX, &errstr);
241228e906dStedu if (errstr)
242228e906dStedu errx(2, "width is %s: %s", errstr, optarg);
243228e906dStedu break;
244228e906dStedu default:
245228e906dStedu usage();
246228e906dStedu }
247228e906dStedu
248228e906dStedu }
249228e906dStedu argc -= optind;
250228e906dStedu argv += optind;
251228e906dStedu
2527d4ecb0aSray if (argc != 2)
25346d91ad5Stedu usage();
25446d91ad5Stedu
255d4ba4722Sray if (outfile && (outfp = fopen(outfile, "w")) == NULL)
256d4ba4722Sray err(2, "could not open: %s", optarg);
257d4ba4722Sray
258127c5611Sray if ((tmpdir = getenv("TMPDIR")) == NULL || *tmpdir == '\0')
25931342e88Sray tmpdir = _PATH_TMP;
26031342e88Sray
2611e22bbb6Sotto filename1 = argv[0];
2621e22bbb6Sotto filename2 = argv[1];
2631e22bbb6Sotto
26432a0d318Smestre if (!Fflag) {
26532a0d318Smestre if (unveil(filename1, "r") == -1)
266bc5a8259Sbeck err(2, "unveil %s", filename1);
26732a0d318Smestre if (unveil(filename2, "r") == -1)
268bc5a8259Sbeck err(2, "unveil %s", filename2);
26932a0d318Smestre if (unveil(tmpdir, "rwc") == -1)
270bc5a8259Sbeck err(2, "unveil %s", tmpdir);
27132a0d318Smestre if (unveil("/usr/bin/diff", "x") == -1)
272bc5a8259Sbeck err(2, "unveil /usr/bin/diff");
27332a0d318Smestre if (unveil(_PATH_BSHELL, "x") == -1)
274bc5a8259Sbeck err(2, "unveil %s", _PATH_BSHELL);
27532a0d318Smestre }
27632a0d318Smestre if (pledge("stdio rpath wpath cpath proc exec", NULL) == -1)
27732a0d318Smestre err(2, "pledge");
27832a0d318Smestre
2791e22bbb6Sotto /*
2801e22bbb6Sotto * Create temporary files for diff and sdiff to share if file1
2811e22bbb6Sotto * or file2 are not regular files. This allows sdiff and diff
2821e22bbb6Sotto * to read the same inputs if one or both inputs are stdin.
2831e22bbb6Sotto *
2841e22bbb6Sotto * If any temporary files were created, their names would be
2851e22bbb6Sotto * saved in tmp1 or tmp2. tmp1 should never equal tmp2.
2861e22bbb6Sotto */
2871e22bbb6Sotto tmp1 = tmp2 = NULL;
2881e22bbb6Sotto /* file1 and file2 are the same, so copy to same temp file. */
2891e22bbb6Sotto if (strcmp(filename1, filename2) == 0) {
2901e22bbb6Sotto if ((tmp1 = mktmpcpy(filename1)))
2911e22bbb6Sotto filename1 = filename2 = tmp1;
2921e22bbb6Sotto /* Copy file1 and file2 into separate temp files. */
2931e22bbb6Sotto } else {
2941e22bbb6Sotto if ((tmp1 = mktmpcpy(filename1)))
2951e22bbb6Sotto filename1 = tmp1;
2961e22bbb6Sotto if ((tmp2 = mktmpcpy(filename2)))
2971e22bbb6Sotto filename2 = tmp2;
2981e22bbb6Sotto }
2991e22bbb6Sotto
3001e22bbb6Sotto diffargv[diffargc++] = filename1;
3011e22bbb6Sotto diffargv[diffargc++] = filename2;
302228e906dStedu /* Add NULL to end of array to indicate end of array. */
303228e906dStedu diffargv[diffargc++] = NULL;
304228e906dStedu
305228e906dStedu /* Subtract column divider and divide by two. */
306228e906dStedu width = (wflag - 3) / 2;
307228e906dStedu /* Make sure line_width can fit in size_t. */
30899c13856Sray if (width > (SIZE_MAX - 3) / 2)
309228e906dStedu errx(2, "width is too large: %zu", width);
310228e906dStedu line_width = width * 2 + 3;
311228e906dStedu
312228e906dStedu if (pipe(fd))
313228e906dStedu err(2, "pipe");
314228e906dStedu
315228e906dStedu switch(pid = fork()) {
316228e906dStedu case 0:
317228e906dStedu /* child */
318228e906dStedu /* We don't read from the pipe. */
319df0f73baStedu close(fd[0]);
320228e906dStedu if (dup2(fd[1], STDOUT_FILENO) == -1)
321228e906dStedu err(2, "child could not duplicate descriptor");
322228e906dStedu /* Free unused descriptor. */
323df0f73baStedu close(fd[1]);
324228e906dStedu
325951d632eSotto execvp(diffprog, diffargv);
326228e906dStedu err(2, "could not execute diff: %s", diffprog);
327228e906dStedu case -1:
328228e906dStedu err(2, "could not fork");
329228e906dStedu }
330228e906dStedu
331228e906dStedu /* parent */
332228e906dStedu /* We don't write to the pipe. */
333df0f73baStedu close(fd[1]);
334228e906dStedu
335228e906dStedu /* Open pipe to diff command. */
33649e0b549Sotto if ((diffpipe = fdopen(fd[0], "r")) == NULL)
337228e906dStedu err(2, "could not open diff pipe");
3381e22bbb6Sotto if ((file1 = fopen(filename1, "r")) == NULL)
3391e22bbb6Sotto err(2, "could not open %s", filename1);
3401e22bbb6Sotto if ((file2 = fopen(filename2, "r")) == NULL)
3411e22bbb6Sotto err(2, "could not open %s", filename2);
3421e22bbb6Sotto
343228e906dStedu /* Line numbers start at one. */
344228e906dStedu file1ln = file2ln = 1;
345228e906dStedu
346228e906dStedu /* Read and parse diff output. */
34749e0b549Sotto while (parsecmd(diffpipe, file1, file2) != EOF)
348228e906dStedu ;
34949e0b549Sotto fclose(diffpipe);
350228e906dStedu
351228e906dStedu /* Wait for diff to exit. */
352228e906dStedu if (waitpid(pid, &status, 0) == -1 || !WIFEXITED(status) ||
353228e906dStedu WEXITSTATUS(status) >= 2)
354228e906dStedu err(2, "diff exited abnormally");
355228e906dStedu
3561e22bbb6Sotto /* Delete and free unneeded temporary files. */
3571e22bbb6Sotto if (tmp1)
3581e22bbb6Sotto if (unlink(tmp1))
3591e22bbb6Sotto warn("error deleting %s", tmp1);
3601e22bbb6Sotto if (tmp2)
3611e22bbb6Sotto if (unlink(tmp2))
3621e22bbb6Sotto warn("error deleting %s", tmp2);
3631e22bbb6Sotto free(tmp1);
3641e22bbb6Sotto free(tmp2);
3651e22bbb6Sotto filename1 = filename2 = tmp1 = tmp2 = NULL;
3661e22bbb6Sotto
367228e906dStedu /* No more diffs, so print common lines. */
36849e0b549Sotto if (lflag)
36949e0b549Sotto while ((s1 = xfgets(file1)))
37049e0b549Sotto enqueue(s1, ' ', NULL);
37149e0b549Sotto else
37249e0b549Sotto for (;;) {
37349e0b549Sotto s1 = xfgets(file1);
37449e0b549Sotto s2 = xfgets(file2);
37549e0b549Sotto if (s1 || s2)
37649e0b549Sotto enqueue(s1, ' ', s2);
37749e0b549Sotto else
37849e0b549Sotto break;
37949e0b549Sotto }
38049e0b549Sotto fclose(file1);
38149e0b549Sotto fclose(file2);
382228e906dStedu /* Process unmodified lines. */
383228e906dStedu processq();
384228e906dStedu
385228e906dStedu /* Return diff exit status. */
386228e906dStedu return (WEXITSTATUS(status));
387228e906dStedu }
388228e906dStedu
389228e906dStedu /*
390228e906dStedu * Prints an individual column (left or right), taking into account
391228e906dStedu * that tabs are variable-width. Takes a string, the current column
392228e906dStedu * the cursor is on the screen, and the maximum value of the column.
393228e906dStedu * The column value is updated as we go along.
394228e906dStedu */
395228e906dStedu static void
printcol(const char * s,size_t * col,const size_t col_max)396228e906dStedu printcol(const char *s, size_t *col, const size_t col_max)
397228e906dStedu {
398228e906dStedu for (; *s && *col < col_max; ++s) {
399228e906dStedu size_t new_col;
400228e906dStedu
401228e906dStedu switch (*s) {
402228e906dStedu case '\t':
403228e906dStedu /*
404228e906dStedu * If rounding to next multiple of eight causes
405228e906dStedu * an integer overflow, just return.
406228e906dStedu */
40799c13856Sray if (*col > SIZE_MAX - 8)
408228e906dStedu return;
409228e906dStedu
410228e906dStedu /* Round to next multiple of eight. */
411228e906dStedu new_col = (*col / 8 + 1) * 8;
412228e906dStedu
413228e906dStedu /*
414228e906dStedu * If printing the tab goes past the column
415228e906dStedu * width, don't print it and just quit.
416228e906dStedu */
417228e906dStedu if (new_col > col_max)
418228e906dStedu return;
419228e906dStedu *col = new_col;
420228e906dStedu break;
421228e906dStedu
422228e906dStedu default:
423228e906dStedu ++(*col);
424228e906dStedu }
425228e906dStedu
426228e906dStedu putchar(*s);
427228e906dStedu }
428228e906dStedu }
429228e906dStedu
430228e906dStedu /*
431228e906dStedu * Prompts user to either choose between two strings or edit one, both,
432228e906dStedu * or neither.
433228e906dStedu */
434228e906dStedu static void
prompt(const char * s1,const char * s2)435228e906dStedu prompt(const char *s1, const char *s2)
436228e906dStedu {
437951d632eSotto char *cmd;
438228e906dStedu
439228e906dStedu /* Print command prompt. */
440228e906dStedu putchar('%');
441228e906dStedu
442228e906dStedu /* Get user input. */
443951d632eSotto for (; (cmd = xfgets(stdin)); free(cmd)) {
444228e906dStedu const char *p;
445228e906dStedu
446228e906dStedu /* Skip leading whitespace. */
44728ce9ad7Sderaadt for (p = cmd; isspace((unsigned char)*p); ++p)
448228e906dStedu ;
449228e906dStedu
450228e906dStedu switch (*p) {
451228e906dStedu case 'e':
452228e906dStedu /* Skip `e'. */
453228e906dStedu ++p;
454228e906dStedu
455228e906dStedu if (eparse(p, s1, s2) == -1)
456228e906dStedu goto USAGE;
457228e906dStedu break;
458228e906dStedu
459228e906dStedu case 'l':
4605e5270dbSray case '1':
461228e906dStedu /* Choose left column as-is. */
462228e906dStedu if (s1 != NULL)
463d4ba4722Sray fprintf(outfp, "%s\n", s1);
464228e906dStedu
465228e906dStedu /* End of command parsing. */
466228e906dStedu break;
467228e906dStedu
468228e906dStedu case 'q':
469228e906dStedu goto QUIT;
470228e906dStedu
471228e906dStedu case 'r':
4725e5270dbSray case '2':
473228e906dStedu /* Choose right column as-is. */
474228e906dStedu if (s2 != NULL)
475d4ba4722Sray fprintf(outfp, "%s\n", s2);
476228e906dStedu
477228e906dStedu /* End of command parsing. */
478228e906dStedu break;
479228e906dStedu
480228e906dStedu case 's':
481228e906dStedu sflag = 1;
482228e906dStedu goto PROMPT;
483228e906dStedu
484228e906dStedu case 'v':
485228e906dStedu sflag = 0;
486228e906dStedu /* FALLTHROUGH */
487228e906dStedu
488228e906dStedu default:
489228e906dStedu /* Interactive usage help. */
490228e906dStedu USAGE:
491228e906dStedu int_usage();
492228e906dStedu PROMPT:
493228e906dStedu putchar('%');
494228e906dStedu
495228e906dStedu /* Prompt user again. */
496228e906dStedu continue;
497228e906dStedu }
498228e906dStedu
499951d632eSotto free(cmd);
500228e906dStedu return;
501228e906dStedu }
502228e906dStedu
503228e906dStedu /*
504228e906dStedu * If there was no error, we received an EOF from stdin, so we
505228e906dStedu * should quit.
506228e906dStedu */
507228e906dStedu QUIT:
508d4ba4722Sray fclose(outfp);
509228e906dStedu exit(0);
510228e906dStedu }
511228e906dStedu
512228e906dStedu /*
513228e906dStedu * Takes two strings, separated by a column divider. NULL strings are
514228e906dStedu * treated as empty columns. If the divider is the ` ' character, the
515228e906dStedu * second column is not printed (-l flag). In this case, the second
516228e906dStedu * string must be NULL. When the second column is NULL, the divider
517228e906dStedu * does not print the trailing space following the divider character.
518228e906dStedu *
519228e906dStedu * Takes into account that tabs can take multiple columns.
520228e906dStedu */
521228e906dStedu static void
println(const char * s1,const char div,const char * s2)522228e906dStedu println(const char *s1, const char div, const char *s2)
523228e906dStedu {
524228e906dStedu size_t col;
525228e906dStedu
526228e906dStedu /* Print first column. Skips if s1 == NULL. */
527228e906dStedu col = 0;
528228e906dStedu if (s1) {
529228e906dStedu /* Skip angle bracket and space. */
530228e906dStedu printcol(s1, &col, width);
531228e906dStedu
532228e906dStedu }
533228e906dStedu
534228e906dStedu /* Only print left column. */
535228e906dStedu if (div == ' ' && !s2) {
536228e906dStedu putchar('\n');
537228e906dStedu return;
538228e906dStedu }
539228e906dStedu
540228e906dStedu /* Otherwise, we pad this column up to width. */
541228e906dStedu for (; col < width; ++col)
542228e906dStedu putchar(' ');
543228e906dStedu
544228e906dStedu /*
545228e906dStedu * Print column divider. If there is no second column, we don't
546228e906dStedu * need to add the space for padding.
547228e906dStedu */
548228e906dStedu if (!s2) {
549228e906dStedu printf(" %c\n", div);
550228e906dStedu return;
551228e906dStedu }
552228e906dStedu printf(" %c ", div);
553228e906dStedu col += 3;
554228e906dStedu
555228e906dStedu /* Skip angle bracket and space. */
556228e906dStedu printcol(s2, &col, line_width);
557228e906dStedu
558228e906dStedu putchar('\n');
559228e906dStedu }
560228e906dStedu
561228e906dStedu /*
562228e906dStedu * Reads a line from file and returns as a string. If EOF is reached,
563228e906dStedu * NULL is returned. The returned string must be freed afterwards.
564228e906dStedu */
565228e906dStedu static char *
xfgets(FILE * file)566228e906dStedu xfgets(FILE *file)
567228e906dStedu {
568228e906dStedu const char delim[3] = {'\0', '\0', '\0'};
569228e906dStedu char *s;
570228e906dStedu
571228e906dStedu /* XXX - Is this necessary? */
572228e906dStedu clearerr(file);
573228e906dStedu
574228e906dStedu if (!(s = fparseln(file, NULL, NULL, delim, 0)) &&
575228e906dStedu ferror(file))
576228e906dStedu err(2, "error reading file");
577228e906dStedu
578228e906dStedu if (!s) {
579228e906dStedu return (NULL);
580228e906dStedu }
581228e906dStedu
582228e906dStedu return (s);
583228e906dStedu }
584228e906dStedu
585228e906dStedu /*
58649e0b549Sotto * Parse ed commands from diffpipe and print lines from file1 (lines
58749e0b549Sotto * to change or delete) or file2 (lines to add or change).
58849e0b549Sotto * Returns EOF or 0.
589228e906dStedu */
590228e906dStedu static int
parsecmd(FILE * diffpipe,FILE * file1,FILE * file2)59149e0b549Sotto parsecmd(FILE *diffpipe, FILE *file1, FILE *file2)
592228e906dStedu {
59349e0b549Sotto size_t file1start, file1end, file2start, file2end, n;
594228e906dStedu /* ed command line and pointer to characters in line */
5955cd71eb4Stedu char *line, *p, *q;
5965cd71eb4Stedu const char *errstr;
5975cd71eb4Stedu char c, cmd;
598228e906dStedu
599228e906dStedu /* Read ed command. */
60049e0b549Sotto if (!(line = xfgets(diffpipe)))
601228e906dStedu return (EOF);
602228e906dStedu
603228e906dStedu p = line;
604228e906dStedu /* Go to character after line number. */
60528ce9ad7Sderaadt while (isdigit((unsigned char)*p))
606228e906dStedu ++p;
6075cd71eb4Stedu c = *p;
6085cd71eb4Stedu *p++ = 0;
6095cd71eb4Stedu file1start = strtonum(line, 0, INT_MAX, &errstr);
6105cd71eb4Stedu if (errstr)
6115cd71eb4Stedu errx(2, "file1 start is %s: %s", errstr, line);
612228e906dStedu
613228e906dStedu /* A range is specified for file1. */
6145cd71eb4Stedu if (c == ',') {
615228e906dStedu
6165cd71eb4Stedu q = p;
617228e906dStedu /* Go to character after file2end. */
61828ce9ad7Sderaadt while (isdigit((unsigned char)*p))
619228e906dStedu ++p;
6205cd71eb4Stedu c = *p;
6215cd71eb4Stedu *p++ = 0;
6225cd71eb4Stedu file1end = strtonum(q, 0, INT_MAX, &errstr);
6235cd71eb4Stedu if (errstr)
6245cd71eb4Stedu errx(2, "file1 end is %s: %s", errstr, line);
6255cd71eb4Stedu if (file1start > file1end)
6265cd71eb4Stedu errx(2, "invalid line range in file1: %s", line);
6275cd71eb4Stedu
628228e906dStedu } else
629228e906dStedu file1end = file1start;
630228e906dStedu
6315cd71eb4Stedu cmd = c;
632228e906dStedu /* Check that cmd is valid. */
633228e906dStedu if (!(cmd == 'a' || cmd == 'c' || cmd == 'd'))
634228e906dStedu errx(2, "ed command not recognized: %c: %s", cmd, line);
635228e906dStedu
6365cd71eb4Stedu q = p;
637228e906dStedu /* Go to character after line number. */
63828ce9ad7Sderaadt while (isdigit((unsigned char)*p))
639228e906dStedu ++p;
6405cd71eb4Stedu c = *p;
6415cd71eb4Stedu *p++ = 0;
6425cd71eb4Stedu file2start = strtonum(q, 0, INT_MAX, &errstr);
6435cd71eb4Stedu if (errstr)
6445cd71eb4Stedu errx(2, "file2 start is %s: %s", errstr, line);
645228e906dStedu
646228e906dStedu /*
647228e906dStedu * There should either be a comma signifying a second line
648228e906dStedu * number or the line should just end here.
649228e906dStedu */
6505cd71eb4Stedu if (c != ',' && c != '\0')
6515cd71eb4Stedu errx(2, "invalid line range in file2: %c: %s", c, line);
652228e906dStedu
6535cd71eb4Stedu if (c == ',') {
654228e906dStedu
6555cd71eb4Stedu file2end = strtonum(p, 0, INT_MAX, &errstr);
6565cd71eb4Stedu if (errstr)
6575cd71eb4Stedu errx(2, "file2 end is %s: %s", errstr, line);
658228e906dStedu if (file2start >= file2end)
659228e906dStedu errx(2, "invalid line range in file2: %s", line);
660228e906dStedu } else
661228e906dStedu file2end = file2start;
662228e906dStedu
663228e906dStedu /* Appends happen _after_ stated line. */
664228e906dStedu if (cmd == 'a') {
665228e906dStedu if (file1start != file1end)
666228e906dStedu errx(2, "append cannot have a file1 range: %s",
667228e906dStedu line);
66899c13856Sray if (file1start == SIZE_MAX)
669228e906dStedu errx(2, "file1 line range too high: %s", line);
670228e906dStedu file1start = ++file1end;
671228e906dStedu }
672228e906dStedu /*
673228e906dStedu * I'm not sure what the deal is with the line numbers for
674228e906dStedu * deletes, though.
675228e906dStedu */
676228e906dStedu else if (cmd == 'd') {
677228e906dStedu if (file2start != file2end)
678228e906dStedu errx(2, "delete cannot have a file2 range: %s",
679228e906dStedu line);
68099c13856Sray if (file2start == SIZE_MAX)
681228e906dStedu errx(2, "file2 line range too high: %s", line);
682228e906dStedu file2start = ++file2end;
683228e906dStedu }
684228e906dStedu
68549e0b549Sotto /*
68649e0b549Sotto * Continue reading file1 and file2 until we reach line numbers
68749e0b549Sotto * specified by diff. Should only happen with -I flag.
68849e0b549Sotto */
68949e0b549Sotto for (; file1ln < file1start && file2ln < file2start;
69049e0b549Sotto ++file1ln, ++file2ln) {
691951d632eSotto char *s1, *s2;
692228e906dStedu
69349e0b549Sotto if (!(s1 = xfgets(file1)))
694228e906dStedu errx(2, "file1 shorter than expected");
69549e0b549Sotto if (!(s2 = xfgets(file2)))
69649e0b549Sotto errx(2, "file2 shorter than expected");
697228e906dStedu
698228e906dStedu /* If the -l flag was specified, print only left column. */
69949e0b549Sotto if (lflag) {
700951d632eSotto free(s2);
70149e0b549Sotto /*
70249e0b549Sotto * XXX - If -l and -I are both specified, all
70349e0b549Sotto * unchanged or ignored lines are shown with a
70449e0b549Sotto * `(' divider. This matches GNU sdiff, but I
70549e0b549Sotto * believe it is a bug. Just check out:
70649e0b549Sotto * gsdiff -l -I '^$' samefile samefile.
70749e0b549Sotto */
70849e0b549Sotto if (Iflag)
70949e0b549Sotto enqueue(s1, '(', NULL);
71049e0b549Sotto else
71149e0b549Sotto enqueue(s1, ' ', NULL);
71249e0b549Sotto } else
71349e0b549Sotto enqueue(s1, ' ', s2);
714228e906dStedu }
71549e0b549Sotto /* Ignore deleted lines. */
71649e0b549Sotto for (; file1ln < file1start; ++file1ln) {
717951d632eSotto char *s;
71849e0b549Sotto
71949e0b549Sotto if (!(s = xfgets(file1)))
72049e0b549Sotto errx(2, "file1 shorter than expected");
72149e0b549Sotto
72249e0b549Sotto enqueue(s, '(', NULL);
72349e0b549Sotto }
72449e0b549Sotto /* Ignore added lines. */
72549e0b549Sotto for (; file2ln < file2start; ++file2ln) {
726951d632eSotto char *s;
72749e0b549Sotto
72849e0b549Sotto if (!(s = xfgets(file2)))
72949e0b549Sotto errx(2, "file2 shorter than expected");
73049e0b549Sotto
73149e0b549Sotto /* If -l flag was given, don't print right column. */
73249e0b549Sotto if (lflag)
733951d632eSotto free(s);
73449e0b549Sotto else
73549e0b549Sotto enqueue(NULL, ')', s);
73649e0b549Sotto }
73749e0b549Sotto
73849e0b549Sotto /* Process unmodified or skipped lines. */
739228e906dStedu processq();
740228e906dStedu
741228e906dStedu switch (cmd) {
742228e906dStedu case 'a':
74349e0b549Sotto printa(file2, file2end);
74449e0b549Sotto n = file2end - file2start + 1;
745228e906dStedu break;
746228e906dStedu
747228e906dStedu case 'c':
74849e0b549Sotto printc(file1, file1end, file2, file2end);
74949e0b549Sotto n = file1end - file1start + 1 + 1 + file2end - file2start + 1;
750228e906dStedu break;
751228e906dStedu
752228e906dStedu case 'd':
75349e0b549Sotto printd(file1, file1end);
75449e0b549Sotto n = file1end - file1start + 1;
755228e906dStedu break;
756228e906dStedu
757228e906dStedu default:
758228e906dStedu errx(2, "invalid diff command: %c: %s", cmd, line);
759228e906dStedu }
7607f148183Stobias free(line);
761228e906dStedu
76249e0b549Sotto /* Skip to next ed line. */
7637f148183Stobias while (n--) {
7647f148183Stobias if (!(line = xfgets(diffpipe)))
76549e0b549Sotto errx(2, "diff ended early");
7667f148183Stobias free(line);
7677f148183Stobias }
76849e0b549Sotto
76949a17b89Stedu return (0);
770228e906dStedu }
771228e906dStedu
772228e906dStedu /*
773228e906dStedu * Queues up a diff line.
774228e906dStedu */
775228e906dStedu static void
enqueue(char * left,char div,char * right)776951d632eSotto enqueue(char *left, char div, char *right)
777228e906dStedu {
778228e906dStedu struct diffline *diffp;
779228e906dStedu
780228e906dStedu if (!(diffp = malloc(sizeof(struct diffline))))
781a9ba38d6Stedu err(2, "enqueue");
782228e906dStedu diffp->left = left;
783228e906dStedu diffp->div = div;
784228e906dStedu diffp->right = right;
785228e906dStedu SIMPLEQ_INSERT_TAIL(&diffhead, diffp, diffentries);
786228e906dStedu }
787228e906dStedu
788228e906dStedu /*
789228e906dStedu * Free a diffline structure and its elements.
790228e906dStedu */
791228e906dStedu static void
freediff(struct diffline * diffp)792951d632eSotto freediff(struct diffline *diffp)
793228e906dStedu {
794951d632eSotto free(diffp->left);
795951d632eSotto free(diffp->right);
796951d632eSotto free(diffp);
797228e906dStedu }
798228e906dStedu
799228e906dStedu /*
800228e906dStedu * Append second string into first. Repeated appends to the same string
801228e906dStedu * are cached, making this an O(n) function, where n = strlen(append).
802228e906dStedu */
803228e906dStedu static void
astrcat(char ** s,const char * append)804228e906dStedu astrcat(char **s, const char *append)
805228e906dStedu {
806228e906dStedu /* Length of string in previous run. */
807228e906dStedu static size_t offset = 0;
80815c8080bSotto size_t newsiz;
809228e906dStedu /*
810228e906dStedu * String from previous run. Compared to *s to see if we are
811228e906dStedu * dealing with the same string. If so, we can use offset.
812228e906dStedu */
813951d632eSotto static const char *oldstr = NULL;
814228e906dStedu char *newstr;
815228e906dStedu
816228e906dStedu
817228e906dStedu /*
818228e906dStedu * First string is NULL, so just copy append.
819228e906dStedu */
820228e906dStedu if (!*s) {
821228e906dStedu if (!(*s = strdup(append)))
822a9ba38d6Stedu err(2, "astrcat");
823228e906dStedu
824228e906dStedu /* Keep track of string. */
825228e906dStedu offset = strlen(*s);
826228e906dStedu oldstr = *s;
827228e906dStedu
828228e906dStedu return;
829228e906dStedu }
830228e906dStedu
831228e906dStedu /*
832228e906dStedu * *s is a string so concatenate.
833228e906dStedu */
834228e906dStedu
835228e906dStedu /* Did we process the same string in the last run? */
836228e906dStedu /*
837228e906dStedu * If this is a different string from the one we just processed
838228e906dStedu * cache new string.
839228e906dStedu */
840228e906dStedu if (oldstr != *s) {
841228e906dStedu offset = strlen(*s);
842228e906dStedu oldstr = *s;
843228e906dStedu }
844228e906dStedu
84515c8080bSotto /* Size = strlen(*s) + \n + strlen(append) + '\0'. */
84615c8080bSotto newsiz = offset + 1 + strlen(append) + 1;
847228e906dStedu
848228e906dStedu /* Resize *s to fit new string. */
84915c8080bSotto newstr = realloc(*s, newsiz);
850228e906dStedu if (newstr == NULL)
851a9ba38d6Stedu err(2, "astrcat");
852228e906dStedu *s = newstr;
853228e906dStedu
85415c8080bSotto /* *s + offset should be end of string. */
855228e906dStedu /* Concatenate. */
85615c8080bSotto strlcpy(*s + offset, "\n", newsiz - offset);
85715c8080bSotto strlcat(*s + offset, append, newsiz - offset);
858228e906dStedu
85915c8080bSotto /* New string length should be exactly newsiz - 1 characters. */
860228e906dStedu /* Store generated string's values. */
86115c8080bSotto offset = newsiz - 1;
862228e906dStedu oldstr = *s;
863228e906dStedu }
864228e906dStedu
865228e906dStedu /*
866228e906dStedu * Process diff set queue, printing, prompting, and saving each diff
867228e906dStedu * line stored in queue.
868228e906dStedu */
869228e906dStedu static void
processq(void)870228e906dStedu processq(void)
871228e906dStedu {
872228e906dStedu struct diffline *diffp;
873951d632eSotto char divc, *left, *right;
874228e906dStedu
875228e906dStedu /* Don't process empty queue. */
876228e906dStedu if (SIMPLEQ_EMPTY(&diffhead))
877228e906dStedu return;
878228e906dStedu
8797bf59f2eSotto /* Remember the divider. */
8807bf59f2eSotto divc = SIMPLEQ_FIRST(&diffhead)->div;
8817bf59f2eSotto
882228e906dStedu left = NULL;
883228e906dStedu right = NULL;
884228e906dStedu /*
885228e906dStedu * Go through set of diffs, concatenating each line in left or
886228e906dStedu * right column into two long strings, `left' and `right'.
887228e906dStedu */
888228e906dStedu SIMPLEQ_FOREACH(diffp, &diffhead, diffentries) {
889228e906dStedu /*
89049e0b549Sotto * Print changed lines if -s was given,
89149e0b549Sotto * print all lines if -s was not given.
892228e906dStedu */
8937bf59f2eSotto if (!sflag || diffp->div == '|' || diffp->div == '<' ||
8947bf59f2eSotto diffp->div == '>')
895228e906dStedu println(diffp->left, diffp->div, diffp->right);
896228e906dStedu
897228e906dStedu /* Append new lines to diff set. */
898228e906dStedu if (diffp->left)
899228e906dStedu astrcat(&left, diffp->left);
900228e906dStedu if (diffp->right)
901228e906dStedu astrcat(&right, diffp->right);
902228e906dStedu }
903228e906dStedu
904228e906dStedu /* Empty queue and free each diff line and its elements. */
905228e906dStedu while (!SIMPLEQ_EMPTY(&diffhead)) {
906228e906dStedu diffp = SIMPLEQ_FIRST(&diffhead);
907228e906dStedu SIMPLEQ_REMOVE_HEAD(&diffhead, diffentries);
90849e0b549Sotto freediff(diffp);
909228e906dStedu }
910228e906dStedu
911d4ba4722Sray /* Write to outfp, prompting user if lines are different. */
912d4ba4722Sray if (outfp)
913951d632eSotto switch (divc) {
91449e0b549Sotto case ' ': case '(': case ')':
915d4ba4722Sray fprintf(outfp, "%s\n", left);
91649e0b549Sotto break;
91749e0b549Sotto case '|': case '<': case '>':
918228e906dStedu prompt(left, right);
91949e0b549Sotto break;
92049e0b549Sotto default:
921951d632eSotto errx(2, "invalid divider: %c", divc);
922228e906dStedu }
923228e906dStedu
924228e906dStedu /* Free left and right. */
925228e906dStedu free(left);
926228e906dStedu free(right);
927228e906dStedu }
928228e906dStedu
929228e906dStedu /*
930228e906dStedu * Print lines following an (a)ppend command.
931228e906dStedu */
932228e906dStedu static void
printa(FILE * file,size_t line2)933228e906dStedu printa(FILE *file, size_t line2)
934228e906dStedu {
935228e906dStedu char *line;
936228e906dStedu
937228e906dStedu for (; file2ln <= line2; ++file2ln) {
938228e906dStedu if (!(line = xfgets(file)))
939228e906dStedu errx(2, "append ended early");
940228e906dStedu enqueue(NULL, '>', line);
941228e906dStedu }
942228e906dStedu
943228e906dStedu processq();
944228e906dStedu }
945228e906dStedu
946228e906dStedu /*
947228e906dStedu * Print lines following a (c)hange command, from file1ln to file1end
948228e906dStedu * and from file2ln to file2end.
949228e906dStedu */
950228e906dStedu static void
printc(FILE * file1,size_t file1end,FILE * file2,size_t file2end)951228e906dStedu printc(FILE *file1, size_t file1end, FILE *file2, size_t file2end)
952228e906dStedu {
953228e906dStedu struct fileline {
954228e906dStedu SIMPLEQ_ENTRY(fileline) fileentries;
955951d632eSotto char *line;
956228e906dStedu };
957228e906dStedu SIMPLEQ_HEAD(, fileline) delqhead = SIMPLEQ_HEAD_INITIALIZER(delqhead);
958228e906dStedu
959228e906dStedu /* Read lines to be deleted. */
960228e906dStedu for (; file1ln <= file1end; ++file1ln) {
961228e906dStedu struct fileline *linep;
962951d632eSotto char *line1;
963228e906dStedu
964228e906dStedu /* Read lines from both. */
965228e906dStedu if (!(line1 = xfgets(file1)))
966228e906dStedu errx(2, "error reading file1 in delete in change");
967228e906dStedu
968228e906dStedu /* Add to delete queue. */
969228e906dStedu if (!(linep = malloc(sizeof(struct fileline))))
970a9ba38d6Stedu err(2, "printc");
971228e906dStedu linep->line = line1;
972228e906dStedu SIMPLEQ_INSERT_TAIL(&delqhead, linep, fileentries);
973228e906dStedu }
974228e906dStedu
975228e906dStedu /* Process changed lines.. */
976228e906dStedu for (; !SIMPLEQ_EMPTY(&delqhead) && file2ln <= file2end;
977228e906dStedu ++file2ln) {
978228e906dStedu struct fileline *del;
979228e906dStedu char *add;
980228e906dStedu
981228e906dStedu /* Get add line. */
98249e0b549Sotto if (!(add = xfgets(file2)))
98349e0b549Sotto errx(2, "error reading add in change");
984228e906dStedu
985228e906dStedu del = SIMPLEQ_FIRST(&delqhead);
986228e906dStedu enqueue(del->line, '|', add);
987228e906dStedu SIMPLEQ_REMOVE_HEAD(&delqhead, fileentries);
988228e906dStedu /*
989228e906dStedu * Free fileline structure but not its elements since
990228e906dStedu * they are queued up.
991228e906dStedu */
992228e906dStedu free(del);
993228e906dStedu }
994228e906dStedu processq();
995228e906dStedu
996228e906dStedu /* Process remaining lines to add. */
997228e906dStedu for (; file2ln <= file2end; ++file2ln) {
998228e906dStedu char *add;
999228e906dStedu
1000228e906dStedu /* Get add line. */
100149e0b549Sotto if (!(add = xfgets(file2)))
100249e0b549Sotto errx(2, "error reading add in change");
1003228e906dStedu
1004228e906dStedu enqueue(NULL, '>', add);
1005228e906dStedu }
1006228e906dStedu processq();
1007228e906dStedu
1008228e906dStedu /* Process remaining lines to delete. */
1009228e906dStedu while (!SIMPLEQ_EMPTY(&delqhead)) {
1010228e906dStedu struct fileline *filep;
1011228e906dStedu
1012228e906dStedu filep = SIMPLEQ_FIRST(&delqhead);
1013228e906dStedu enqueue(filep->line, '<', NULL);
1014228e906dStedu SIMPLEQ_REMOVE_HEAD(&delqhead, fileentries);
1015228e906dStedu free(filep);
1016228e906dStedu }
1017228e906dStedu processq();
1018228e906dStedu }
1019228e906dStedu
1020228e906dStedu /*
1021228e906dStedu * Print deleted lines from file, from file1ln to file1end.
1022228e906dStedu */
1023228e906dStedu static void
printd(FILE * file1,size_t file1end)102449e0b549Sotto printd(FILE *file1, size_t file1end)
1025228e906dStedu {
1026951d632eSotto char *line1;
1027228e906dStedu
1028228e906dStedu /* Print out lines file1ln to line2. */
1029228e906dStedu for (; file1ln <= file1end; ++file1ln) {
1030228e906dStedu if (!(line1 = xfgets(file1)))
1031228e906dStedu errx(2, "file1 ended early in delete");
1032228e906dStedu enqueue(line1, '<', NULL);
1033228e906dStedu }
1034228e906dStedu processq();
1035228e906dStedu }
1036228e906dStedu
1037228e906dStedu /*
1038228e906dStedu * Interactive mode usage.
1039228e906dStedu */
1040228e906dStedu static void
int_usage(void)1041228e906dStedu int_usage(void)
1042228e906dStedu {
1043228e906dStedu puts("e:\tedit blank diff\n"
1044228e906dStedu "eb:\tedit both diffs concatenated\n"
1045228e906dStedu "el:\tedit left diff\n"
1046228e906dStedu "er:\tedit right diff\n"
10475e5270dbSray "l | 1:\tchoose left diff\n"
10485e5270dbSray "r | 2:\tchoose right diff\n"
1049228e906dStedu "s:\tsilent mode--don't print identical lines\n"
1050228e906dStedu "v:\tverbose mode--print identical lines\n"
1051228e906dStedu "q:\tquit");
1052228e906dStedu }
1053228e906dStedu
1054228e906dStedu static void
usage(void)1055228e906dStedu usage(void)
1056228e906dStedu {
1057228e906dStedu extern char *__progname;
1058228e906dStedu
1059228e906dStedu fprintf(stderr,
1060618b0722Stedu "usage: %s [-abdilstW] [-I regexp] [-o outfile] [-w width] file1 file2\n",
1061228e906dStedu __progname);
1062228e906dStedu exit(2);
1063228e906dStedu }
1064