Home | History | Annotate | Line # | Download | only in diff
      1 /*	$OpenBSD: diff.c,v 1.67 2019/06/28 13:35:00 deraadt Exp $	*/
      2 
      3 /*
      4  * Copyright (c) 2003 Todd C. Miller <Todd.Miller (at) courtesan.com>
      5  *
      6  * Permission to use, copy, modify, and distribute this software for any
      7  * purpose with or without fee is hereby granted, provided that the above
      8  * copyright notice and this permission notice appear in all copies.
      9  *
     10  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
     11  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
     12  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
     13  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
     14  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
     15  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
     16  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
     17  *
     18  * Sponsored in part by the Defense Advanced Research Projects
     19  * Agency (DARPA) and Air Force Research Laboratory, Air Force
     20  * Materiel Command, USAF, under agreement number F39502-99-1-0512.
     21  */
     22 
     23 #include <sys/cdefs.h>
     24 #include <sys/stat.h>
     25 
     26 #include <ctype.h>
     27 #include <err.h>
     28 #include <errno.h>
     29 #include <getopt.h>
     30 #include <stdlib.h>
     31 #include <stdio.h>
     32 #include <string.h>
     33 #include <unistd.h>
     34 #include <limits.h>
     35 
     36 #include "diff.h"
     37 #include "xmalloc.h"
     38 
     39 bool	 lflag, Nflag, Pflag, rflag, sflag, Tflag, cflag;
     40 bool	 ignore_file_case, suppress_common, brief_diff;
     41 int	 diff_format, diff_context, status;
     42 int	 width = 130;
     43 char	*start, *ifdefname, *diffargs, *label[2];
     44 char	*ignore_pats, *most_recent_pat;
     45 char	*group_format = NULL;
     46 const char	*add_code, *del_code;
     47 struct stat stb1, stb2;
     48 struct excludes *excludes_list;
     49 regex_t	 ignore_re, most_recent_re;
     50 
     51 #define	OPTIONS	"0123456789aBbC:cdD:efF:HhI:iL:lnNPpqrS:sTtU:uwW:X:x:y"
     52 enum {
     53 	OPT_TSIZE = CHAR_MAX + 1,
     54 	OPT_STRIPCR,
     55 	OPT_IGN_FN_CASE,
     56 	OPT_NO_IGN_FN_CASE,
     57 	OPT_NORMAL,
     58 	OPT_HELP,
     59 	OPT_HORIZON_LINES,
     60 	OPT_CHANGED_GROUP_FORMAT,
     61 	OPT_SUPPRESS_COMMON,
     62 	OPT_VERSION,
     63 };
     64 
     65 static struct option longopts[] = {
     66 	{ "text",			no_argument,		0,	'a' },
     67 	{ "ignore-space-change",	no_argument,		0,	'b' },
     68 	{ "context",			optional_argument,	0,	'C' },
     69 	{ "ifdef",			required_argument,	0,	'D' },
     70 	{ "minimal",			no_argument,		0,	'd' },
     71 	{ "ed",				no_argument,		0,	'e' },
     72 	{ "forward-ed",			no_argument,		0,	'f' },
     73 	{ "show-function-line",		required_argument,	0,	'F' },
     74 	{ "speed-large-files",		no_argument,		NULL,	'H' },
     75 	{ "ignore-blank-lines",		no_argument,		0,	'B' },
     76 	{ "ignore-matching-lines",	required_argument,	0,	'I' },
     77 	{ "ignore-case",		no_argument,		0,	'i' },
     78 	{ "paginate",			no_argument,		NULL,	'l' },
     79 	{ "label",			required_argument,	0,	'L' },
     80 	{ "new-file",			no_argument,		0,	'N' },
     81 	{ "rcs",			no_argument,		0,	'n' },
     82 	{ "unidirectional-new-file",	no_argument,		0,	'P' },
     83 	{ "show-c-function",		no_argument,		0,	'p' },
     84 	{ "brief",			no_argument,		0,	'q' },
     85 	{ "recursive",			no_argument,		0,	'r' },
     86 	{ "report-identical-files",	no_argument,		0,	's' },
     87 	{ "starting-file",		required_argument,	0,	'S' },
     88 	{ "expand-tabs",		no_argument,		0,	't' },
     89 	{ "initial-tab",		no_argument,		0,	'T' },
     90 	{ "unified",			optional_argument,	0,	'U' },
     91 	{ "ignore-all-space",		no_argument,		0,	'w' },
     92 	{ "width",			required_argument,	0,	'W' },
     93 	{ "exclude",			required_argument,	0,	'x' },
     94 	{ "exclude-from",		required_argument,	0,	'X' },
     95 	{ "side-by-side",		no_argument,		NULL,	'y' },
     96 	{ "ignore-file-name-case",	no_argument,		NULL,	OPT_IGN_FN_CASE },
     97 	{ "horizon-lines",		required_argument,	NULL,	OPT_HORIZON_LINES },
     98 	{ "no-ignore-file-name-case",	no_argument,		NULL,	OPT_NO_IGN_FN_CASE },
     99 	{ "normal",			no_argument,		NULL,	OPT_NORMAL },
    100 	{ "strip-trailing-cr",		no_argument,		NULL,	OPT_STRIPCR },
    101 	{ "changed-group-format",	required_argument,	NULL,	OPT_CHANGED_GROUP_FORMAT},
    102 	{ "suppress-common-lines",	no_argument,		NULL,	OPT_SUPPRESS_COMMON },
    103 	{ NULL,				0,			0,	'\0'}
    104 };
    105 
    106 static void checked_regcomp(char const *, regex_t *);
    107 static void usage(void) __dead;
    108 static void conflicting_format(void) __dead;
    109 static void push_excludes(char *);
    110 static void push_ignore_pats(char *);
    111 static void read_excludes_file(char *file);
    112 static void set_argstr(char **, char **);
    113 static char *splice(char *, char *);
    114 
    115 int
    116 main(int argc, char **argv)
    117 {
    118 	char *ep, **oargv;
    119 	long  l;
    120 	int   ch, dflags, lastch, gotstdin, prevoptind, newarg;
    121 
    122 	oargv = argv;
    123 	gotstdin = 0;
    124 	dflags = 0;
    125 	lastch = '\0';
    126 	prevoptind = 1;
    127 	newarg = 1;
    128 	diff_context = 3;
    129 	diff_format = D_UNSET;
    130 #define	FORMAT_MISMATCHED(type)	\
    131 	(diff_format != D_UNSET && diff_format != (type))
    132 	while ((ch = getopt_long(argc, argv, OPTIONS, longopts, NULL)) != -1) {
    133 		switch (ch) {
    134 		case '0': case '1': case '2': case '3': case '4':
    135 		case '5': case '6': case '7': case '8': case '9':
    136 			if (newarg)
    137 				usage();	/* disallow -[0-9]+ */
    138 			else if (lastch == 'c' || lastch == 'u')
    139 				diff_context = 0;
    140 			else if (!isdigit(lastch) || diff_context > INT_MAX / 10)
    141 				usage();
    142 			diff_context = (diff_context * 10) + (ch - '0');
    143 			break;
    144 		case 'a':
    145 			dflags |= D_FORCEASCII;
    146 			break;
    147 		case 'b':
    148 			dflags |= D_FOLDBLANKS;
    149 			break;
    150 		case 'C':
    151 		case 'c':
    152 			if (FORMAT_MISMATCHED(D_CONTEXT))
    153 				conflicting_format();
    154 			cflag = true;
    155 			diff_format = D_CONTEXT;
    156 			if (optarg != NULL) {
    157 				l = strtol(optarg, &ep, 10);
    158 				if (*ep != '\0' || l < 0 || l >= INT_MAX)
    159 					usage();
    160 				diff_context = (int)l;
    161 			}
    162 			break;
    163 		case 'd':
    164 			dflags |= D_MINIMAL;
    165 			break;
    166 		case 'D':
    167 			if (FORMAT_MISMATCHED(D_IFDEF))
    168 				conflicting_format();
    169 			diff_format = D_IFDEF;
    170 			ifdefname = optarg;
    171 			break;
    172 		case 'e':
    173 			if (FORMAT_MISMATCHED(D_EDIT))
    174 				conflicting_format();
    175 			diff_format = D_EDIT;
    176 			break;
    177 		case 'f':
    178 			if (FORMAT_MISMATCHED(D_REVERSE))
    179 				conflicting_format();
    180 			diff_format = D_REVERSE;
    181 			break;
    182 		case 'H':
    183 			/* ignore but needed for compatibility with GNU diff */
    184 			break;
    185 		case 'h':
    186 			/* silently ignore for backwards compatibility */
    187 			break;
    188 		case 'B':
    189 			dflags |= D_SKIPBLANKLINES;
    190 			break;
    191 		case 'F':
    192 			if (dflags & D_PROTOTYPE)
    193 				conflicting_format();
    194 			dflags |= D_MATCHLAST;
    195 			most_recent_pat = xstrdup(optarg);
    196 			break;
    197 		case 'I':
    198 			push_ignore_pats(optarg);
    199 			break;
    200 		case 'i':
    201 			dflags |= D_IGNORECASE;
    202 			break;
    203 		case 'L':
    204 			if (label[0] == NULL)
    205 				label[0] = optarg;
    206 			else if (label[1] == NULL)
    207 				label[1] = optarg;
    208 			else
    209 				usage();
    210 			break;
    211 		case 'l':
    212 			lflag = true;
    213 			break;
    214 		case 'N':
    215 			Nflag = true;
    216 			break;
    217 		case 'n':
    218 			if (FORMAT_MISMATCHED(D_NREVERSE))
    219 				conflicting_format();
    220 			diff_format = D_NREVERSE;
    221 			break;
    222 		case 'p':
    223 			if (dflags & D_MATCHLAST)
    224 				conflicting_format();
    225 			dflags |= D_PROTOTYPE;
    226 			break;
    227 		case 'P':
    228 			Pflag = true;
    229 			break;
    230 		case 'r':
    231 			rflag = true;
    232 			break;
    233 		case 'q':
    234 			brief_diff = true;
    235 			break;
    236 		case 'S':
    237 			start = optarg;
    238 			break;
    239 		case 's':
    240 			sflag = true;
    241 			break;
    242 		case 'T':
    243 			Tflag = true;
    244 			break;
    245 		case 't':
    246 			dflags |= D_EXPANDTABS;
    247 			break;
    248 		case 'U':
    249 		case 'u':
    250 			if (FORMAT_MISMATCHED(D_UNIFIED))
    251 				conflicting_format();
    252 			diff_format = D_UNIFIED;
    253 			if (optarg != NULL) {
    254 				l = strtol(optarg, &ep, 10);
    255 				if (*ep != '\0' || l < 0 || l >= INT_MAX)
    256 					usage();
    257 				diff_context = (int)l;
    258 			}
    259 			break;
    260 		case 'w':
    261 			dflags |= D_IGNOREBLANKS;
    262 			break;
    263 		case 'W':
    264 			if (optarg != NULL) {
    265 				l = strtol(optarg, &ep, 10);
    266 				if (*ep != '\0' || l < 0 || l >= INT_MAX)
    267 					usage();
    268 				width = (int)l;
    269 			} else {
    270 				usage();
    271 			}
    272 			break;
    273 		case 'X':
    274 			read_excludes_file(optarg);
    275 			break;
    276 		case 'x':
    277 			push_excludes(optarg);
    278 			break;
    279 		case 'y':
    280 			if (FORMAT_MISMATCHED(D_SIDEBYSIDE))
    281 				conflicting_format();
    282 			diff_format = D_SIDEBYSIDE;
    283 			break;
    284 		case OPT_CHANGED_GROUP_FORMAT:
    285 			if (FORMAT_MISMATCHED(D_GFORMAT))
    286 				conflicting_format();
    287 			diff_format = D_GFORMAT;
    288 			group_format = optarg;
    289 			break;
    290 		case OPT_HORIZON_LINES:
    291 			break; /* XXX TODO for compatibility with GNU diff3 */
    292 		case OPT_IGN_FN_CASE:
    293 			ignore_file_case = true;
    294 			break;
    295 		case OPT_NO_IGN_FN_CASE:
    296 			ignore_file_case = false;
    297 			break;
    298 		case OPT_NORMAL:
    299 			if (FORMAT_MISMATCHED(D_NORMAL))
    300 				conflicting_format();
    301 			diff_format = D_NORMAL;
    302 			break;
    303 		case OPT_STRIPCR:
    304 			dflags |= D_STRIPCR;
    305 			break;
    306 		case OPT_SUPPRESS_COMMON:
    307 			suppress_common = 1;
    308 			break;
    309 		default:
    310 			usage();
    311 			break;
    312 		}
    313 		lastch = ch;
    314 		newarg = optind != prevoptind;
    315 		prevoptind = optind;
    316 	}
    317 	if (diff_format == D_UNSET && (dflags & D_PROTOTYPE) != 0)
    318 		diff_format = D_CONTEXT;
    319 	if (diff_format == D_UNSET)
    320 		diff_format = D_NORMAL;
    321 	argc -= optind;
    322 	argv += optind;
    323 
    324 	/*
    325 	 * Do sanity checks, fill in stb1 and stb2 and call the appropriate
    326 	 * driver routine.  Both drivers use the contents of stb1 and stb2.
    327 	 */
    328 	if (argc != 2)
    329 		usage();
    330 	checked_regcomp(ignore_pats, &ignore_re);
    331 	checked_regcomp(most_recent_pat, &most_recent_re);
    332 	if (strcmp(argv[0], "-") == 0) {
    333 		fstat(STDIN_FILENO, &stb1);
    334 		gotstdin = 1;
    335 	} else if (stat(argv[0], &stb1) != 0) {
    336 		if (!Nflag || errno != ENOENT)
    337 			err(2, "%s", argv[0]);
    338 		dflags |= D_EMPTY1;
    339 		memset(&stb1, 0, sizeof(struct stat));
    340 	}
    341 
    342 	if (strcmp(argv[1], "-") == 0) {
    343 		fstat(STDIN_FILENO, &stb2);
    344 		gotstdin = 1;
    345 	} else if (stat(argv[1], &stb2) != 0) {
    346 		if (!Nflag || errno != ENOENT)
    347 			err(2, "%s", argv[1]);
    348 		dflags |= D_EMPTY2;
    349 		memset(&stb2, 0, sizeof(stb2));
    350 		stb2.st_mode = stb1.st_mode;
    351 	}
    352 
    353 	if (dflags & D_EMPTY1 && dflags & D_EMPTY2){
    354 		warn("%s", argv[0]);
    355 		warn("%s", argv[1]);
    356 		exit(2);
    357 	}
    358 
    359 	if (stb1.st_mode == 0)
    360 		stb1.st_mode = stb2.st_mode;
    361 
    362 	if (gotstdin && (S_ISDIR(stb1.st_mode) || S_ISDIR(stb2.st_mode)))
    363 		errx(2, "can't compare - to a directory");
    364 	set_argstr(oargv, argv);
    365 	if (S_ISDIR(stb1.st_mode) && S_ISDIR(stb2.st_mode)) {
    366 		if (diff_format == D_IFDEF)
    367 			errx(2, "-D option not supported with directories");
    368 		diffdir(argv[0], argv[1], dflags);
    369 	} else {
    370 		if (S_ISDIR(stb1.st_mode)) {
    371 			argv[0] = splice(argv[0], argv[1]);
    372 			if (stat(argv[0], &stb1) == -1)
    373 				err(2, "%s", argv[0]);
    374 		}
    375 		if (S_ISDIR(stb2.st_mode)) {
    376 			argv[1] = splice(argv[1], argv[0]);
    377 			if (stat(argv[1], &stb2) == -1)
    378 				err(2, "%s", argv[1]);
    379 		}
    380 		print_status(diffreg(argv[0], argv[1], dflags), argv[0],
    381 		    argv[1], "");
    382 	}
    383 	exit(status);
    384 }
    385 
    386 static void
    387 checked_regcomp(char const *pattern, regex_t *comp)
    388 {
    389 	char buf[BUFSIZ];
    390 	int error;
    391 
    392 	if (pattern == NULL)
    393 		return;
    394 
    395 	error = regcomp(comp, pattern, REG_NEWLINE | REG_EXTENDED);
    396 	if (error != 0) {
    397 		regerror(error, comp, buf, sizeof(buf));
    398 		if (*pattern != '\0')
    399 			errx(2, "%s: %s", pattern, buf);
    400 		else
    401 			errx(2, "%s", buf);
    402 	}
    403 }
    404 
    405 static void
    406 set_argstr(char **av, char **ave)
    407 {
    408 	size_t argsize;
    409 	char **ap;
    410 
    411 	argsize = 4 + *ave - *av + 1;
    412 	diffargs = xmalloc(argsize);
    413 	strlcpy(diffargs, "diff", argsize);
    414 	for (ap = av + 1; ap < ave; ap++) {
    415 		if (strcmp(*ap, "--") != 0) {
    416 			strlcat(diffargs, " ", argsize);
    417 			strlcat(diffargs, *ap, argsize);
    418 		}
    419 	}
    420 }
    421 
    422 /*
    423  * Read in an excludes file and push each line.
    424  */
    425 static void
    426 read_excludes_file(char *file)
    427 {
    428 	FILE *fp;
    429 	char *buf, *pattern;
    430 	size_t len;
    431 
    432 	if (strcmp(file, "-") == 0)
    433 		fp = stdin;
    434 	else if ((fp = fopen(file, "r")) == NULL)
    435 		err(2, "%s", file);
    436 	while ((buf = fgetln(fp, &len)) != NULL) {
    437 		if (buf[len - 1] == '\n')
    438 			len--;
    439 		if ((pattern = strndup(buf, len)) == NULL)
    440 			err(2, "xstrndup");
    441 		push_excludes(pattern);
    442 	}
    443 	if (strcmp(file, "-") != 0)
    444 		fclose(fp);
    445 }
    446 
    447 /*
    448  * Push a pattern onto the excludes list.
    449  */
    450 static void
    451 push_excludes(char *pattern)
    452 {
    453 	struct excludes *entry;
    454 
    455 	entry = xmalloc(sizeof(*entry));
    456 	entry->pattern = pattern;
    457 	entry->next = excludes_list;
    458 	excludes_list = entry;
    459 }
    460 
    461 static void
    462 push_ignore_pats(char *pattern)
    463 {
    464 	size_t len;
    465 
    466 	if (ignore_pats == NULL)
    467 		ignore_pats = xstrdup(pattern);
    468 	else {
    469 		/* old + "|" + new + NUL */
    470 		len = strlen(ignore_pats) + strlen(pattern) + 2;
    471 		ignore_pats = xreallocarray(ignore_pats, 1, len);
    472 		strlcat(ignore_pats, "|", len);
    473 		strlcat(ignore_pats, pattern, len);
    474 	}
    475 }
    476 
    477 void
    478 print_status(int val, char *path1, char *path2, const char *entry)
    479 {
    480 	if (label[0] != NULL)
    481 		path1 = label[0];
    482 	if (label[1] != NULL)
    483 		path2 = label[1];
    484 
    485 	switch (val) {
    486 	case D_BINARY:
    487 		printf("Binary files %s%s and %s%s differ\n",
    488 		    path1, entry, path2, entry);
    489 		break;
    490 	case D_DIFFER:
    491 		if (brief_diff)
    492 			printf("Files %s%s and %s%s differ\n",
    493 			    path1, entry, path2, entry);
    494 		break;
    495 	case D_SAME:
    496 		if (sflag)
    497 			printf("Files %s%s and %s%s are identical\n",
    498 			    path1, entry, path2, entry);
    499 		break;
    500 	case D_MISMATCH1:
    501 		printf("File %s%s is a directory while file %s%s is a regular file\n",
    502 		    path1, entry, path2, entry);
    503 		break;
    504 	case D_MISMATCH2:
    505 		printf("File %s%s is a regular file while file %s%s is a directory\n",
    506 		    path1, entry, path2, entry);
    507 		break;
    508 	case D_SKIPPED1:
    509 		printf("File %s%s is not a regular file or directory and was skipped\n",
    510 		    path1, entry);
    511 		break;
    512 	case D_SKIPPED2:
    513 		printf("File %s%s is not a regular file or directory and was skipped\n",
    514 		    path2, entry);
    515 		break;
    516 	case D_ERROR:
    517 		break;
    518 	}
    519 }
    520 
    521 static void
    522 usage(void)
    523 {
    524 	(void)fprintf(stderr,
    525 	    "usage: diff [-aBbdilpTtw] [-c | -e | -f | -n | -q | -u] [--ignore-case]\n"
    526 	    "            [--no-ignore-case] [--normal] [--strip-trailing-cr]\n"
    527 	    "            [-I pattern] [-F pattern] [-L label] file1 file2\n"
    528 	    "       diff [-aBbdilpTtw] [-I pattern] [-L label] [--ignore-case]\n"
    529 	    "            [--no-ignore-case] [--normal] [--strip-trailing-cr]\n"
    530 	    "            [-F pattern] -C number file1 file2\n"
    531 	    "       diff [-aBbdiltw] [-I pattern] [--ignore-case] [--no-ignore-case]\n"
    532 	    "            [--normal] [--strip-trailing-cr] -D string file1 file2\n"
    533 	    "       diff [-aBbdilpTtw] [-I pattern] [-L label] [--ignore-case]\n"
    534 	    "            [--no-ignore-case] [--normal] [--strip-trailing-cr]\n"
    535 	    "            [-F pattern] -U number file1 file2\n"
    536 	    "       diff [-aBbdilNPprsTtw] [-c | -e | -f | -n | -q | -u] [--ignore-case]\n"
    537 	    "            [--no-ignore-case] [--normal] [-I pattern] [-L label]\n"
    538 	    "            [-F pattern] [-S name] [-X file] [-x pattern] dir1 dir2\n"
    539 	    "       diff [-aBbditwW] [--expand-tabs] [--ignore-all-space]\n"
    540 	    "            [--ignore-blank-lines] [--ignore-case] [--minimal]\n"
    541 	    "            [--no-ignore-file-name-case] [--strip-trailing-cr]\n"
    542 	    "            [--suppress-common-lines] [--text] [--width]\n"
    543 	    "            -y | --side-by-side file1 file2\n");
    544 
    545 	exit(2);
    546 }
    547 
    548 static void
    549 conflicting_format(void)
    550 {
    551 
    552 	fprintf(stderr, "error: conflicting output format options.\n");
    553 	usage();
    554 }
    555 
    556 static char *
    557 splice(char *dir, char *path)
    558 {
    559 	char *tail, *buf;
    560 	size_t dirlen;
    561 
    562 	dirlen = strlen(dir);
    563 	while (dirlen != 0 && dir[dirlen - 1] == '/')
    564 	    dirlen--;
    565 	if ((tail = strrchr(path, '/')) == NULL)
    566 		tail = path;
    567 	else
    568 		tail++;
    569 	xasprintf(&buf, "%.*s/%s", (int)dirlen, dir, tail);
    570 	return (buf);
    571 }
    572