/* GNU diff - compare files line by line Copyright (C) 1988-1989, 1992-1994, 1996, 1998, 2001-2002, 2004, 2006-2007, 2009-2013, 2015-2023 Free Software Foundation, Inc. This file is part of GNU DIFF. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #define GDIFF_MAIN #include "diff.h" #include "die.h" #include #include "paths.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* The official name of this program (e.g., no 'g' prefix). */ static char const PROGRAM_NAME[] = "diff"; #define AUTHORS \ proper_name ("Paul Eggert"), \ proper_name ("Mike Haertel"), \ proper_name ("David Hayes"), \ proper_name ("Richard Stallman"), \ proper_name ("Len Tower") #ifndef GUTTER_WIDTH_MINIMUM # define GUTTER_WIDTH_MINIMUM 3 #endif struct regexp_list { char *regexps; /* chars representing disjunction of the regexps */ size_t len; /* chars used in 'regexps' */ size_t size; /* size malloc'ed for 'regexps'; 0 if not malloc'ed */ bool multiple_regexps;/* Does 'regexps' represent a disjunction? */ struct re_pattern_buffer *buf; }; static int compare_files (struct comparison const *, char const *, char const *); static void add_regexp (struct regexp_list *, char const *); static void summarize_regexp_list (struct regexp_list *); static void specify_style (enum output_style); static void specify_value (char const **, char const *, char const *); static void specify_colors_style (char const *); static _Noreturn void try_help (char const *, char const *); static void check_stdout (void); static void usage (void); /* If comparing directories, compare their common subdirectories recursively. */ static bool recursive; /* In context diffs, show previous lines that match these regexps. */ static struct regexp_list function_regexp_list; /* Ignore changes affecting only lines that match these regexps. */ static struct regexp_list ignore_regexp_list; #if O_BINARY /* Use binary I/O when reading and writing data (--binary). On POSIX hosts, this has no effect. */ static bool binary; #else enum { binary = true }; #endif /* If one file is missing, treat it as present but empty (-N). */ static bool new_file; /* If the first file is missing, treat it as present but empty (--unidirectional-new-file). */ static bool unidirectional_new_file; /* Report files compared that are the same (-s). Normally nothing is output when that happens. */ static bool report_identical_files; /* Do not treat directories specially. */ static bool no_directory; static char const shortopts[] = "0123456789abBcC:dD:eEfF:hHiI:lL:nNpPqrsS:tTuU:vwW:x:X:yZ"; /* Values for long options that do not have single-letter equivalents. */ enum { BINARY_OPTION = CHAR_MAX + 1, FROM_FILE_OPTION, HELP_OPTION, HORIZON_LINES_OPTION, IGNORE_FILE_NAME_CASE_OPTION, INHIBIT_HUNK_MERGE_OPTION, LEFT_COLUMN_OPTION, LINE_FORMAT_OPTION, NO_DEREFERENCE_OPTION, NO_IGNORE_FILE_NAME_CASE_OPTION, NORMAL_OPTION, SDIFF_MERGE_ASSIST_OPTION, STRIP_TRAILING_CR_OPTION, SUPPRESS_BLANK_EMPTY_OPTION, SUPPRESS_COMMON_LINES_OPTION, TABSIZE_OPTION, TO_FILE_OPTION, /* These options must be in sequence. */ UNCHANGED_LINE_FORMAT_OPTION, OLD_LINE_FORMAT_OPTION, NEW_LINE_FORMAT_OPTION, /* These options must be in sequence. */ UNCHANGED_GROUP_FORMAT_OPTION, OLD_GROUP_FORMAT_OPTION, NEW_GROUP_FORMAT_OPTION, CHANGED_GROUP_FORMAT_OPTION, COLOR_OPTION, COLOR_PALETTE_OPTION, NO_DIRECTORY_OPTION, PRESUME_OUTPUT_TTY_OPTION, }; static char const group_format_option[][sizeof "--unchanged-group-format"] = { "--unchanged-group-format", "--old-group-format", "--new-group-format", "--changed-group-format" }; static char const line_format_option[][sizeof "--unchanged-line-format"] = { "--unchanged-line-format", "--old-line-format", "--new-line-format" }; static struct option const longopts[] = { {"binary", 0, 0, BINARY_OPTION}, {"brief", 0, 0, 'q'}, {"changed-group-format", 1, 0, CHANGED_GROUP_FORMAT_OPTION}, {"color", 2, 0, COLOR_OPTION}, {"context", 2, 0, 'C'}, {"ed", 0, 0, 'e'}, {"exclude", 1, 0, 'x'}, {"exclude-from", 1, 0, 'X'}, {"expand-tabs", 0, 0, 't'}, {"forward-ed", 0, 0, 'f'}, {"from-file", 1, 0, FROM_FILE_OPTION}, {"help", 0, 0, HELP_OPTION}, {"horizon-lines", 1, 0, HORIZON_LINES_OPTION}, {"ifdef", 1, 0, 'D'}, {"ignore-all-space", 0, 0, 'w'}, {"ignore-blank-lines", 0, 0, 'B'}, {"ignore-case", 0, 0, 'i'}, {"ignore-file-name-case", 0, 0, IGNORE_FILE_NAME_CASE_OPTION}, {"ignore-matching-lines", 1, 0, 'I'}, {"ignore-space-change", 0, 0, 'b'}, {"ignore-tab-expansion", 0, 0, 'E'}, {"ignore-trailing-space", 0, 0, 'Z'}, {"inhibit-hunk-merge", 0, 0, INHIBIT_HUNK_MERGE_OPTION}, {"initial-tab", 0, 0, 'T'}, {"label", 1, 0, 'L'}, {"left-column", 0, 0, LEFT_COLUMN_OPTION}, {"line-format", 1, 0, LINE_FORMAT_OPTION}, {"minimal", 0, 0, 'd'}, {"new-file", 0, 0, 'N'}, {"new-group-format", 1, 0, NEW_GROUP_FORMAT_OPTION}, {"new-line-format", 1, 0, NEW_LINE_FORMAT_OPTION}, {"no-dereference", 0, 0, NO_DEREFERENCE_OPTION}, {"no-ignore-file-name-case", 0, 0, NO_IGNORE_FILE_NAME_CASE_OPTION}, {"normal", 0, 0, NORMAL_OPTION}, {"old-group-format", 1, 0, OLD_GROUP_FORMAT_OPTION}, {"old-line-format", 1, 0, OLD_LINE_FORMAT_OPTION}, {"paginate", 0, 0, 'l'}, {"palette", 1, 0, COLOR_PALETTE_OPTION}, {"rcs", 0, 0, 'n'}, {"recursive", 0, 0, 'r'}, {"report-identical-files", 0, 0, 's'}, {"sdiff-merge-assist", 0, 0, SDIFF_MERGE_ASSIST_OPTION}, {"show-c-function", 0, 0, 'p'}, {"show-function-line", 1, 0, 'F'}, {"side-by-side", 0, 0, 'y'}, {"speed-large-files", 0, 0, 'H'}, {"starting-file", 1, 0, 'S'}, {"strip-trailing-cr", 0, 0, STRIP_TRAILING_CR_OPTION}, {"suppress-blank-empty", 0, 0, SUPPRESS_BLANK_EMPTY_OPTION}, {"suppress-common-lines", 0, 0, SUPPRESS_COMMON_LINES_OPTION}, {"tabsize", 1, 0, TABSIZE_OPTION}, {"text", 0, 0, 'a'}, {"to-file", 1, 0, TO_FILE_OPTION}, {"unchanged-group-format", 1, 0, UNCHANGED_GROUP_FORMAT_OPTION}, {"unchanged-line-format", 1, 0, UNCHANGED_LINE_FORMAT_OPTION}, {"unidirectional-new-file", 0, 0, 'P'}, {"unified", 2, 0, 'U'}, {"version", 0, 0, 'v'}, {"width", 1, 0, 'W'}, /* This is solely for diff3. Do not document. */ {"-no-directory", no_argument, nullptr, NO_DIRECTORY_OPTION}, /* This is solely for testing. Do not document. */ {"-presume-output-tty", no_argument, nullptr, PRESUME_OUTPUT_TTY_OPTION}, {0, 0, 0, 0} }; /* Return a string containing the command options with which diff was invoked. Spaces appear between what were separate ARGV-elements. There is a space at the beginning but none at the end. If there were no options, the result is an empty string. Arguments: OPTIONVEC, a vector containing separate ARGV-elements, and COUNT, the length of that vector. */ static char * option_list (char **optionvec, int count) { int i; size_t size = 1; char *result; char *p; for (i = 0; i < count; i++) { size_t optsize = 1 + shell_quote_length (optionvec[i]); if (INT_ADD_WRAPV (optsize, size, &size)) xalloc_die (); } p = result = xmalloc (size); for (i = 0; i < count; i++) { *p++ = ' '; p = shell_quote_copy (p, optionvec[i]); } *p = '\0'; return result; } /* Return an option value suitable for add_exclude. */ static int exclude_options (void) { return EXCLUDE_WILDCARDS | (ignore_file_name_case ? FNM_CASEFOLD : 0); } int main (int argc, char **argv) { int exit_status = EXIT_SUCCESS; int c; int i; int prev = -1; lin ocontext = -1; bool explicit_context = false; size_t width = 0; bool show_c_function = false; char const *from_file = nullptr; char const *to_file = nullptr; intmax_t numval; char *numend; /* Do our initializations. */ exit_failure = EXIT_TROUBLE; initialize_main (&argc, &argv); set_program_name (argv[0]); setlocale (LC_ALL, ""); bindtextdomain (PACKAGE, LOCALEDIR); textdomain (PACKAGE); c_stack_action (0); function_regexp_list.buf = &function_regexp; ignore_regexp_list.buf = &ignore_regexp; re_set_syntax (RE_SYNTAX_GREP | RE_NO_POSIX_BACKTRACKING); excluded = new_exclude (); presume_output_tty = false; xstdopen (); /* Decode the options. */ while ((c = getopt_long (argc, argv, shortopts, longopts, nullptr)) != -1) { switch (c) { case 0: break; case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': ocontext = (! ISDIGIT (prev) ? c - '0' : (ocontext - (c - '0' <= CONTEXT_MAX % 10) < CONTEXT_MAX / 10) ? 10 * ocontext + (c - '0') : CONTEXT_MAX); break; case 'a': text = true; break; case 'b': if (ignore_white_space < IGNORE_SPACE_CHANGE) ignore_white_space = IGNORE_SPACE_CHANGE; break; case 'Z': if (ignore_white_space < IGNORE_SPACE_CHANGE) ignore_white_space |= IGNORE_TRAILING_SPACE; break; case 'B': ignore_blank_lines = true; break; case 'C': case 'U': { if (optarg) { numval = strtoimax (optarg, &numend, 10); if (*numend || numval < 0) try_help ("invalid context length '%s'", optarg); if (CONTEXT_MAX < numval) numval = CONTEXT_MAX; } else numval = 3; specify_style (c == 'U' ? OUTPUT_UNIFIED : OUTPUT_CONTEXT); if (context < numval) context = numval; explicit_context = true; } break; case 'c': specify_style (OUTPUT_CONTEXT); if (context < 3) context = 3; break; case 'd': minimal = true; break; case 'D': specify_style (OUTPUT_IFDEF); { static char const C_ifdef_group_formats[] = (/* UNCHANGED */ "%=" "\0" /* OLD */ "#ifndef @\n" "%<" "#endif /* ! @ */\n" "\0" /* NEW */ "#ifdef @\n" "%>" "#endif /* @ */\n" "\0" /* CHANGED */ "#ifndef @\n" "%<" "#else /* @ */\n" "%>" "#endif /* @ */\n"); size_t alloc = strlen (optarg); if (INT_MULTIPLY_WRAPV (alloc, 7, &alloc) || INT_ADD_WRAPV (alloc, sizeof C_ifdef_group_formats - 7 /* 7*"@" */, &alloc)) xalloc_die (); char *b = xmalloc (alloc); char *base = b; int changes = 0; for (i = 0; i < sizeof C_ifdef_group_formats; i++) { char ch = C_ifdef_group_formats[i]; switch (ch) { default: *b++ = ch; break; case '@': b = stpcpy (b, optarg); break; case '\0': *b++ = ch; specify_value (&group_format[changes++], base, "-D"); base = b; break; } } } break; case 'e': specify_style (OUTPUT_ED); break; case 'E': if (ignore_white_space < IGNORE_SPACE_CHANGE) ignore_white_space |= IGNORE_TAB_EXPANSION; break; case 'f': specify_style (OUTPUT_FORWARD_ED); break; case 'F': add_regexp (&function_regexp_list, optarg); break; case 'h': /* Split the files into chunks for faster processing. Usually does not change the result. This currently has no effect. */ break; case 'H': speed_large_files = true; break; case 'i': ignore_case = true; break; case 'I': add_regexp (&ignore_regexp_list, optarg); break; case 'l': if (!pr_program[0]) try_help ("pagination not supported on this host", nullptr); paginate = true; #ifdef SIGCHLD /* Pagination requires forking and waiting, and System V fork+wait does not work if SIGCHLD is ignored. */ signal (SIGCHLD, SIG_DFL); #endif break; case 'L': if (!file_label[0]) file_label[0] = optarg; else if (!file_label[1]) file_label[1] = optarg; else fatal ("too many file label options"); break; case 'n': specify_style (OUTPUT_RCS); break; case 'N': new_file = true; break; case 'p': show_c_function = true; add_regexp (&function_regexp_list, "^[[:alpha:]$_]"); break; case 'P': unidirectional_new_file = true; break; case 'q': brief = true; break; case 'r': recursive = true; break; case 's': report_identical_files = true; break; case 'S': specify_value (&starting_file, optarg, "-S"); break; case 't': expand_tabs = true; break; case 'T': initial_tab = true; break; case 'u': specify_style (OUTPUT_UNIFIED); if (context < 3) context = 3; break; case 'v': version_etc (stdout, PROGRAM_NAME, PACKAGE_NAME, Version, AUTHORS, nullptr); check_stdout (); return EXIT_SUCCESS; case 'w': ignore_white_space = IGNORE_ALL_SPACE; break; case 'x': add_exclude (excluded, optarg, exclude_options ()); break; case 'X': if (add_exclude_file (add_exclude, excluded, optarg, exclude_options (), '\n')) pfatal_with_name (optarg); break; case 'y': specify_style (OUTPUT_SDIFF); break; case 'W': numval = strtoimax (optarg, &numend, 10); if (! (0 < numval && numval <= SIZE_MAX) || *numend) try_help ("invalid width '%s'", optarg); if (width != numval) { if (width) fatal ("conflicting width options"); width = numval; } break; case BINARY_OPTION: #if O_BINARY binary = true; if (! isatty (STDOUT_FILENO)) set_binary_mode (STDOUT_FILENO, O_BINARY); #endif break; case FROM_FILE_OPTION: specify_value (&from_file, optarg, "--from-file"); break; case HELP_OPTION: usage (); check_stdout (); return EXIT_SUCCESS; case HORIZON_LINES_OPTION: numval = strtoimax (optarg, &numend, 10); if (*numend || numval < 0) try_help ("invalid horizon length '%s'", optarg); horizon_lines = MAX (horizon_lines, MIN (numval, LIN_MAX)); break; case IGNORE_FILE_NAME_CASE_OPTION: ignore_file_name_case = true; break; case INHIBIT_HUNK_MERGE_OPTION: /* This option is obsolete, but accept it for backward compatibility. */ break; case LEFT_COLUMN_OPTION: left_column = true; break; case LINE_FORMAT_OPTION: specify_style (OUTPUT_IFDEF); for (i = 0; i < sizeof line_format / sizeof line_format[0]; i++) specify_value (&line_format[i], optarg, "--line-format"); break; case NO_DEREFERENCE_OPTION: no_dereference_symlinks = true; break; case NO_IGNORE_FILE_NAME_CASE_OPTION: ignore_file_name_case = false; break; case NORMAL_OPTION: specify_style (OUTPUT_NORMAL); break; case SDIFF_MERGE_ASSIST_OPTION: specify_style (OUTPUT_SDIFF); sdiff_merge_assist = true; break; case STRIP_TRAILING_CR_OPTION: strip_trailing_cr = true; break; case SUPPRESS_BLANK_EMPTY_OPTION: suppress_blank_empty = true; break; case SUPPRESS_COMMON_LINES_OPTION: suppress_common_lines = true; break; case TABSIZE_OPTION: numval = strtoimax (optarg, &numend, 10); if (! (0 < numval && numval <= SIZE_MAX - GUTTER_WIDTH_MINIMUM) || *numend) try_help ("invalid tabsize '%s'", optarg); if (tabsize != numval) { if (tabsize) fatal ("conflicting tabsize options"); tabsize = numval; } break; case TO_FILE_OPTION: specify_value (&to_file, optarg, "--to-file"); break; case UNCHANGED_LINE_FORMAT_OPTION: case OLD_LINE_FORMAT_OPTION: case NEW_LINE_FORMAT_OPTION: specify_style (OUTPUT_IFDEF); c -= UNCHANGED_LINE_FORMAT_OPTION; specify_value (&line_format[c], optarg, line_format_option[c]); break; case UNCHANGED_GROUP_FORMAT_OPTION: case OLD_GROUP_FORMAT_OPTION: case NEW_GROUP_FORMAT_OPTION: case CHANGED_GROUP_FORMAT_OPTION: specify_style (OUTPUT_IFDEF); c -= UNCHANGED_GROUP_FORMAT_OPTION; specify_value (&group_format[c], optarg, group_format_option[c]); break; case COLOR_OPTION: specify_colors_style (optarg); break; case COLOR_PALETTE_OPTION: set_color_palette (optarg); break; case NO_DIRECTORY_OPTION: no_directory = true; break; case PRESUME_OUTPUT_TTY_OPTION: presume_output_tty = true; break; default: try_help (nullptr, nullptr); } prev = c; } if (colors_style == AUTO) { char const *t = getenv ("TERM"); if (t && STREQ (t, "dumb")) colors_style = NEVER; } if (output_style == OUTPUT_UNSPECIFIED) { if (show_c_function) { specify_style (OUTPUT_CONTEXT); if (ocontext < 0) context = 3; } else specify_style (OUTPUT_NORMAL); } if (output_style != OUTPUT_CONTEXT || hard_locale (LC_TIME)) { #if (defined STAT_TIMESPEC || defined STAT_TIMESPEC_NS \ || defined HAVE_STRUCT_STAT_ST_SPARE1) time_format = "%Y-%m-%d %H:%M:%S.%N %z"; #else time_format = "%Y-%m-%d %H:%M:%S %z"; #endif #if !HAVE_TM_GMTOFF localtz = tzalloc (getenv ("TZ")); #endif } else { /* See POSIX 1003.1-2001 for this format. */ time_format = "%a %b %e %T %Y"; } if (0 <= ocontext && (output_style == OUTPUT_CONTEXT || output_style == OUTPUT_UNIFIED) && (context < ocontext || (ocontext < context && ! explicit_context))) context = ocontext; if (! tabsize) tabsize = 8; if (! width) width = 130; { /* Maximize first the half line width, and then the gutter width, according to the following constraints: 1. Two half lines plus a gutter must fit in a line. 2. If the half line width is nonzero: a. The gutter width is at least GUTTER_WIDTH_MINIMUM. b. If tabs are not expanded to spaces, a half line plus a gutter is an integral number of tabs, so that tabs in the right column line up. */ size_t t = expand_tabs ? 1 : tabsize; size_t w = width; size_t t_plus_g = t + GUTTER_WIDTH_MINIMUM; size_t unaligned_off = (w >> 1) + (t_plus_g >> 1) + (w & t_plus_g & 1); size_t off = unaligned_off - unaligned_off % t; sdiff_half_width = (off <= GUTTER_WIDTH_MINIMUM || w <= off ? 0 : MIN (off - GUTTER_WIDTH_MINIMUM, w - off)); sdiff_column2_offset = sdiff_half_width ? off : w; } /* Make the horizon at least as large as the context, so that shift_boundaries has more freedom to shift the first and last hunks. */ if (horizon_lines < context) horizon_lines = context; summarize_regexp_list (&function_regexp_list); summarize_regexp_list (&ignore_regexp_list); if (output_style == OUTPUT_IFDEF) { for (i = 0; i < sizeof line_format / sizeof line_format[0]; i++) if (!line_format[i]) line_format[i] = "%l\n"; if (!group_format[OLD]) group_format[OLD] = group_format[CHANGED] ? group_format[CHANGED] : "%<"; if (!group_format[NEW]) group_format[NEW] = group_format[CHANGED] ? group_format[CHANGED] : "%>"; if (!group_format[UNCHANGED]) group_format[UNCHANGED] = "%="; if (!group_format[CHANGED]) { char *p = xmalloc (strlen (group_format[OLD]) + strlen (group_format[NEW]) + 1); group_format[CHANGED] = p; strcpy (stpcpy (p, group_format[OLD]), group_format[NEW]); } } no_diff_means_no_output = (output_style == OUTPUT_IFDEF ? (!*group_format[UNCHANGED] || (STREQ (group_format[UNCHANGED], "%=") && !*line_format[UNCHANGED])) : (output_style != OUTPUT_SDIFF) | suppress_common_lines); files_can_be_treated_as_binary = (brief & binary & ~ (ignore_blank_lines | ignore_case | strip_trailing_cr | (ignore_regexp_list.regexps || ignore_white_space))); switch_string = option_list (argv + 1, optind - 1); if (from_file) { if (to_file) fatal ("--from-file and --to-file both specified"); else for (; optind < argc; optind++) { int status = compare_files (nullptr, from_file, argv[optind]); if (exit_status < status) exit_status = status; } } else { if (to_file) for (; optind < argc; optind++) { int status = compare_files (nullptr, argv[optind], to_file); if (exit_status < status) exit_status = status; } else { if (argc - optind != 2) { if (argc - optind < 2) try_help ("missing operand after '%s'", argv[argc - 1]); else try_help ("extra operand '%s'", argv[optind + 2]); } exit_status = compare_files (nullptr, argv[optind], argv[optind + 1]); } } /* Print any messages that were saved up for last. */ print_message_queue (); check_stdout (); cleanup_signal_handlers (); return exit_status; } /* Append to REGLIST the regexp PATTERN. */ static void add_regexp (struct regexp_list *reglist, char const *pattern) { size_t patlen = strlen (pattern); char const *m = re_compile_pattern (pattern, patlen, reglist->buf); if (m != 0) error (EXIT_TROUBLE, 0, "%s: %s", pattern, m); else { char *regexps = reglist->regexps; size_t len = reglist->len; bool multiple_regexps = reglist->multiple_regexps = regexps != 0; size_t newlen = reglist->len = len + 2 * multiple_regexps + patlen; size_t size = reglist->size; if (size <= newlen) { if (!size) size = 1; do size *= 2; while (size <= newlen); reglist->size = size; reglist->regexps = regexps = xrealloc (regexps, size); } if (multiple_regexps) { regexps[len++] = '\\'; regexps[len++] = '|'; } memcpy (regexps + len, pattern, patlen + 1); } } /* Ensure that REGLIST represents the disjunction of its regexps. This is done here, rather than earlier, to avoid O(N^2) behavior. */ static void summarize_regexp_list (struct regexp_list *reglist) { if (reglist->regexps) { /* At least one regexp was specified. Allocate a fastmap for it. */ reglist->buf->fastmap = xmalloc (1 << CHAR_BIT); if (reglist->multiple_regexps) { /* Compile the disjunction of the regexps. (If just one regexp was specified, it is already compiled.) */ char const *m = re_compile_pattern (reglist->regexps, reglist->len, reglist->buf); if (m) die (EXIT_TROUBLE, 0, "%s: %s", reglist->regexps, m); } } } static void try_help (char const *reason_msgid, char const *operand) { if (reason_msgid) error (0, 0, _(reason_msgid), operand); die (EXIT_TROUBLE, 0, _("Try '%s --help' for more information."), program_name); } static void check_stdout (void) { if (ferror (stdout)) fatal ("write failed"); else if (fclose (stdout) != 0) pfatal_with_name (_("standard output")); } static char const * const option_help_msgid[] = { N_(" --normal output a normal diff (the default)"), N_("-q, --brief report only when files differ"), N_("-s, --report-identical-files report when two files are the same"), N_("-c, -C NUM, --context[=NUM] output NUM (default 3) lines of copied context"), N_("-u, -U NUM, --unified[=NUM] output NUM (default 3) lines of unified context"), N_("-e, --ed output an ed script"), N_("-n, --rcs output an RCS format diff"), N_("-y, --side-by-side output in two columns"), N_("-W, --width=NUM output at most NUM (default 130) print columns"), N_(" --left-column output only the left column of common lines"), N_(" --suppress-common-lines do not output common lines"), "", N_("-p, --show-c-function show which C function each change is in"), N_("-F, --show-function-line=RE show the most recent line matching RE"), N_(" --label LABEL use LABEL instead of file name and timestamp\n" " (can be repeated)"), "", N_("-t, --expand-tabs expand tabs to spaces in output"), N_("-T, --initial-tab make tabs line up by prepending a tab"), N_(" --tabsize=NUM tab stops every NUM (default 8) print columns"), N_(" --suppress-blank-empty suppress space or tab before empty output lines"), N_("-l, --paginate pass output through 'pr' to paginate it"), "", N_("-r, --recursive recursively compare any subdirectories found"), N_(" --no-dereference don't follow symbolic links"), N_("-N, --new-file treat absent files as empty"), N_(" --unidirectional-new-file treat absent first files as empty"), N_(" --ignore-file-name-case ignore case when comparing file names"), N_(" --no-ignore-file-name-case consider case when comparing file names"), N_("-x, --exclude=PAT exclude files that match PAT"), N_("-X, --exclude-from=FILE exclude files that match any pattern in FILE"), N_("-S, --starting-file=FILE start with FILE when comparing directories"), N_(" --from-file=FILE1 compare FILE1 to all operands;\n" " FILE1 can be a directory"), N_(" --to-file=FILE2 compare all operands to FILE2;\n" " FILE2 can be a directory"), "", N_("-i, --ignore-case ignore case differences in file contents"), N_("-E, --ignore-tab-expansion ignore changes due to tab expansion"), N_("-Z, --ignore-trailing-space ignore white space at line end"), N_("-b, --ignore-space-change ignore changes in the amount of white space"), N_("-w, --ignore-all-space ignore all white space"), N_("-B, --ignore-blank-lines ignore changes where lines are all blank"), N_("-I, --ignore-matching-lines=RE ignore changes where all lines match RE"), "", N_("-a, --text treat all files as text"), N_(" --strip-trailing-cr strip trailing carriage return on input"), #if O_BINARY N_(" --binary read and write data in binary mode"), #endif "", N_("-D, --ifdef=NAME output merged file with '#ifdef NAME' diffs"), N_(" --GTYPE-group-format=GFMT format GTYPE input groups with GFMT"), N_(" --line-format=LFMT format all input lines with LFMT"), N_(" --LTYPE-line-format=LFMT format LTYPE input lines with LFMT"), N_(" These format options provide fine-grained control over the output\n" " of diff, generalizing -D/--ifdef."), N_(" LTYPE is 'old', 'new', or 'unchanged'. GTYPE is LTYPE or 'changed'."), N_(" GFMT (only) may contain:\n\ %< lines from FILE1\n\ %> lines from FILE2\n\ %= lines common to FILE1 and FILE2\n\ %[-][WIDTH][.[PREC]]{doxX}LETTER printf-style spec for LETTER\n\ LETTERs are as follows for new group, lower case for old group:\n\ F first line number\n\ L last line number\n\ N number of lines = L-F+1\n\ E F-1\n\ M L+1\n\ %(A=B?T:E) if A equals B then T else E"), N_(" LFMT (only) may contain:\n\ %L contents of line\n\ %l contents of line, excluding any trailing newline\n\ %[-][WIDTH][.[PREC]]{doxX}n printf-style spec for input line number"), N_(" Both GFMT and LFMT may contain:\n\ %% %\n\ %c'C' the single character C\n\ %c'\\OOO' the character with octal code OOO\n\ C the character C (other characters represent themselves)"), "", N_("-d, --minimal try hard to find a smaller set of changes"), N_(" --horizon-lines=NUM keep NUM lines of the common prefix and suffix"), N_(" --speed-large-files assume large files and many scattered small changes"), N_(" --color[=WHEN] color output; WHEN is 'never', 'always', or 'auto';\n" " plain --color means --color='auto'"), N_(" --palette=PALETTE the colors to use when --color is active; PALETTE is\n" " a colon-separated list of terminfo capabilities"), "", N_(" --help display this help and exit"), N_("-v, --version output version information and exit"), "", N_("FILES are 'FILE1 FILE2' or 'DIR1 DIR2' or 'DIR FILE' or 'FILE DIR'."), N_("If --from-file or --to-file is given, there are no restrictions on FILE(s)."), N_("If a FILE is '-', read standard input."), N_("Exit status is 0 if inputs are the same, 1 if different, 2 if trouble."), 0 }; static void usage (void) { char const * const *p; printf (_("Usage: %s [OPTION]... FILES\n"), program_name); printf ("%s\n\n", _("Compare FILES line by line.")); fputs (_("\ Mandatory arguments to long options are mandatory for short options too.\n\ "), stdout); for (p = option_help_msgid; *p; p++) { if (!**p) putchar ('\n'); else { char const *msg = _(*p); char const *nl; while ((nl = strchr (msg, '\n'))) { int msglen = nl + 1 - msg; /* This assertion is solely to avoid a warning from gcc's -Wformat-overflow=. */ assert (msglen < 4096); printf (" %.*s", msglen, msg); msg = nl + 1; } printf (&" %s\n"[2 * (*msg != ' ' && *msg != '-')], msg); } } emit_bug_reporting_address (); } /* Set VAR to VALUE, reporting an OPTION error if this is a conflict. */ static void specify_value (char const **var, char const *value, char const *option) { if (*var && ! STREQ (*var, value)) { error (0, 0, _("conflicting %s option value '%s'"), option, value); try_help (nullptr, nullptr); } *var = value; } /* Set the output style to STYLE, diagnosing conflicts. */ static void specify_style (enum output_style style) { if (output_style != style) { if (output_style != OUTPUT_UNSPECIFIED) try_help ("conflicting output style options", nullptr); output_style = style; } } /* Set the color mode. */ static void specify_colors_style (char const *value) { if (value == nullptr || STREQ (value, "auto")) colors_style = AUTO; else if (STREQ (value, "always")) colors_style = ALWAYS; else if (STREQ (value, "never")) colors_style = NEVER; else try_help ("invalid color '%s'", value); } /* Set the last-modified time of *ST to be the current time. */ static void set_mtime_to_now (struct stat *st) { #ifdef STAT_TIMESPEC gettime (&STAT_TIMESPEC (st, st_mtim)); #else struct timespec t; gettime (&t); st->st_mtime = t.tv_sec; # if defined STAT_TIMESPEC_NS STAT_TIMESPEC_NS (st, st_mtim) = t.tv_nsec; # elif defined HAVE_STRUCT_STAT_ST_SPARE1 st->st_spare1 = t.tv_nsec / 1000; # endif #endif } /* cmp.file[f].desc markers */ enum { NONEXISTENT = -1 }; /* nonexistent file */ enum { UNOPENED = -2 }; /* unopened file (e.g. directory) */ /* encoded errno value */ static int errno_encode (int err) { return -3 - err; } /* inverse of errno_encode */ static int errno_decode (int desc) { return -3 - desc; } /* Compare two files (or dirs) with parent comparison PARENT and names NAME0 and NAME1. (If PARENT is null, then the first name is just NAME0, etc.) This is self-contained; it opens the files and closes them. Value is EXIT_SUCCESS if files are the same, EXIT_FAILURE if different, EXIT_TROUBLE if there is a problem opening them. */ static int compare_files (struct comparison const *parent, char const *name0, char const *name1) { struct comparison cmp; #define DIR_P(f) (S_ISDIR (cmp.file[f].stat.st_mode) != 0) register int f; int status = EXIT_SUCCESS; bool same_files; char *free0; char *free1; /* If this is directory comparison, perhaps we have a file that exists only in one of the directories. If so, just print a message to that effect. */ if (! ((name0 && name1) || (unidirectional_new_file && name1) || new_file)) { char const *name = name0 ? name0 : name1; char const *dir = parent->file[!name0].name; /* See POSIX 1003.1-2001 for this format. */ message ("Only in %s: %s\n", dir, name); /* Return EXIT_FAILURE so that diff_dirs will return EXIT_FAILURE ("some files differ"). */ return EXIT_FAILURE; } memset (cmp.file, 0, sizeof cmp.file); cmp.parent = parent; cmp.file[0].desc = name0 ? UNOPENED : NONEXISTENT; cmp.file[1].desc = name1 ? UNOPENED : NONEXISTENT; /* Now record the full name of each file, including nonexistent ones. */ if (!name0) name0 = name1; if (!name1) name1 = name0; if (!parent) { free0 = nullptr; free1 = nullptr; cmp.file[0].name = name0; cmp.file[1].name = name1; } else { cmp.file[0].name = free0 = file_name_concat (parent->file[0].name, name0, nullptr); cmp.file[1].name = free1 = file_name_concat (parent->file[1].name, name1, nullptr); } /* Stat the files. */ for (f = 0; f < 2; f++) { if (cmp.file[f].desc != NONEXISTENT) { if (f && file_name_cmp (cmp.file[f].name, cmp.file[0].name) == 0) { cmp.file[f].desc = cmp.file[0].desc; cmp.file[f].stat = cmp.file[0].stat; } else if (STREQ (cmp.file[f].name, "-")) { cmp.file[f].desc = STDIN_FILENO; if (binary && ! isatty (STDIN_FILENO)) set_binary_mode (STDIN_FILENO, O_BINARY); if (fstat (STDIN_FILENO, &cmp.file[f].stat) != 0) cmp.file[f].desc = errno_encode (errno); else { if (S_ISREG (cmp.file[f].stat.st_mode)) { off_t pos = lseek (STDIN_FILENO, 0, SEEK_CUR); if (pos < 0) cmp.file[f].desc = errno_encode (errno); else cmp.file[f].stat.st_size = MAX (0, cmp.file[f].stat.st_size - pos); } /* POSIX 1003.1-2001 requires current time for stdin. */ set_mtime_to_now (&cmp.file[f].stat); } } else if ((no_dereference_symlinks ? lstat (cmp.file[f].name, &cmp.file[f].stat) : stat (cmp.file[f].name, &cmp.file[f].stat)) != 0) cmp.file[f].desc = errno_encode (errno); } } /* Mark files as nonexistent as needed for -N and -P, if they are inaccessible empty regular files (the kind of files that 'patch' creates to indicate nonexistent backups), or if they are top-level files that do not exist but their counterparts do exist. */ for (f = 0; f < 2; f++) if ((new_file || (f == 0 && unidirectional_new_file)) && (cmp.file[f].desc == UNOPENED ? (S_ISREG (cmp.file[f].stat.st_mode) && ! (cmp.file[f].stat.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO)) && cmp.file[f].stat.st_size == 0) : ((cmp.file[f].desc == errno_encode (ENOENT) || cmp.file[f].desc == errno_encode (EBADF)) && ! parent && (cmp.file[1 - f].desc == UNOPENED || cmp.file[1 - f].desc == STDIN_FILENO)))) cmp.file[f].desc = NONEXISTENT; for (f = 0; f < 2; f++) if (cmp.file[f].desc == NONEXISTENT) { memset (&cmp.file[f].stat, 0, sizeof cmp.file[f].stat); cmp.file[f].stat.st_mode = cmp.file[1 - f].stat.st_mode; } for (f = 0; f < 2; f++) { int e = errno_decode (cmp.file[f].desc); if (0 <= e) { errno = e; perror_with_name (cmp.file[f].name); status = EXIT_TROUBLE; } } if (status == EXIT_SUCCESS && ! parent && !no_directory && DIR_P (0) != DIR_P (1)) { /* If one is a directory, and it was specified in the command line, use the file in that dir with the other file's basename. */ int fnm_arg = DIR_P (0); int dir_arg = 1 - fnm_arg; char const *fnm = cmp.file[fnm_arg].name; char const *dir = cmp.file[dir_arg].name; char const *filename = cmp.file[dir_arg].name = free0 = find_dir_file_pathname (dir, last_component (fnm)); if (STREQ (fnm, "-")) fatal ("cannot compare '-' to a directory"); if ((no_dereference_symlinks ? lstat (filename, &cmp.file[dir_arg].stat) : stat (filename, &cmp.file[dir_arg].stat)) != 0) { perror_with_name (filename); status = EXIT_TROUBLE; } } if (status != EXIT_SUCCESS) { /* One of the files should exist but does not. */ } else if (cmp.file[0].desc == NONEXISTENT && cmp.file[1].desc == NONEXISTENT) { /* Neither file "exists", so there's nothing to compare. */ } else if ((same_files = (cmp.file[0].desc != NONEXISTENT && cmp.file[1].desc != NONEXISTENT && 0 < same_file (&cmp.file[0].stat, &cmp.file[1].stat) && same_file_attributes (&cmp.file[0].stat, &cmp.file[1].stat))) && no_diff_means_no_output) { /* The two named files are actually the same physical file. We know they are identical without actually reading them. */ } else if (DIR_P (0) & DIR_P (1)) { if (output_style == OUTPUT_IFDEF) fatal ("-D option not supported with directories"); /* If both are directories, compare the files in them. */ if (parent && !recursive) { /* But don't compare dir contents one level down unless -r was specified. See POSIX 1003.1-2001 for this format. */ message ("Common subdirectories: %s and %s\n", cmp.file[0].name, cmp.file[1].name); } else status = diff_dirs (&cmp, compare_files); } else if ((DIR_P (0) | DIR_P (1)) || (parent && !((S_ISREG (cmp.file[0].stat.st_mode) || S_ISLNK (cmp.file[0].stat.st_mode)) && (S_ISREG (cmp.file[1].stat.st_mode) || S_ISLNK (cmp.file[1].stat.st_mode))))) { if (cmp.file[0].desc == NONEXISTENT || cmp.file[1].desc == NONEXISTENT) { /* We have a subdirectory that exists only in one directory. */ if ((DIR_P (0) | DIR_P (1)) && recursive && (new_file || (unidirectional_new_file && cmp.file[0].desc == NONEXISTENT))) status = diff_dirs (&cmp, compare_files); else { char const *dir; /* PARENT must be non-null here. */ assert (parent); dir = parent->file[cmp.file[0].desc == NONEXISTENT].name; /* See POSIX 1003.1-2001 for this format. */ message ("Only in %s: %s\n", dir, name0); status = EXIT_FAILURE; } } else { /* We have two files that are not to be compared. */ /* See POSIX 1003.1-2001 for this format. */ message ("File %s is a %s while file %s is a %s\n", file_label[0] ? file_label[0] : cmp.file[0].name, file_type (&cmp.file[0].stat), file_label[1] ? file_label[1] : cmp.file[1].name, file_type (&cmp.file[1].stat)); /* This is a difference. */ status = EXIT_FAILURE; } } else if (S_ISLNK (cmp.file[0].stat.st_mode) || S_ISLNK (cmp.file[1].stat.st_mode)) { /* We get here only if we use lstat(), not stat(). */ assert (no_dereference_symlinks); if (S_ISLNK (cmp.file[0].stat.st_mode) && S_ISLNK (cmp.file[1].stat.st_mode)) { /* Compare the values of the symbolic links. */ char *link_value[2] = { nullptr, nullptr }; for (f = 0; f < 2; f++) { link_value[f] = xreadlink (cmp.file[f].name); if (link_value[f] == nullptr) { perror_with_name (cmp.file[f].name); status = EXIT_TROUBLE; break; } } if (status == EXIT_SUCCESS) { if ( ! STREQ (link_value[0], link_value[1])) { message ("Symbolic links %s and %s differ\n", cmp.file[0].name, cmp.file[1].name); /* This is a difference. */ status = EXIT_FAILURE; } } for (f = 0; f < 2; f++) free (link_value[f]); } else { /* We have two files that are not to be compared, because one of them is a symbolic link and the other one is not. */ message ("File %s is a %s while file %s is a %s\n", file_label[0] ? file_label[0] : cmp.file[0].name, file_type (&cmp.file[0].stat), file_label[1] ? file_label[1] : cmp.file[1].name, file_type (&cmp.file[1].stat)); /* This is a difference. */ status = EXIT_FAILURE; } } else if (files_can_be_treated_as_binary && S_ISREG (cmp.file[0].stat.st_mode) && S_ISREG (cmp.file[1].stat.st_mode) && cmp.file[0].stat.st_size != cmp.file[1].stat.st_size && 0 < cmp.file[0].stat.st_size && 0 < cmp.file[1].stat.st_size) { message ("Files %s and %s differ\n", file_label[0] ? file_label[0] : cmp.file[0].name, file_label[1] ? file_label[1] : cmp.file[1].name); status = EXIT_FAILURE; } else { /* Both exist and neither is a directory. */ /* Open the files and record their descriptors. */ int oflags = O_RDONLY | (binary ? O_BINARY : 0); if (cmp.file[0].desc == UNOPENED) if ((cmp.file[0].desc = open (cmp.file[0].name, oflags, 0)) < 0) { perror_with_name (cmp.file[0].name); status = EXIT_TROUBLE; } if (cmp.file[1].desc == UNOPENED) { if (same_files) cmp.file[1].desc = cmp.file[0].desc; else if ((cmp.file[1].desc = open (cmp.file[1].name, oflags, 0)) < 0) { perror_with_name (cmp.file[1].name); status = EXIT_TROUBLE; } } /* Compare the files, if no error was found. */ if (status == EXIT_SUCCESS) status = diff_2_files (&cmp); /* Close the file descriptors. */ if (0 <= cmp.file[0].desc && close (cmp.file[0].desc) != 0) { perror_with_name (cmp.file[0].name); status = EXIT_TROUBLE; } if (0 <= cmp.file[1].desc && cmp.file[0].desc != cmp.file[1].desc && close (cmp.file[1].desc) != 0) { perror_with_name (cmp.file[1].name); status = EXIT_TROUBLE; } } /* Now the comparison has been done, if no error prevented it, and STATUS is the value this function will return. */ if (status == EXIT_SUCCESS) { if (report_identical_files && !DIR_P (0)) message ("Files %s and %s are identical\n", file_label[0] ? file_label[0] : cmp.file[0].name, file_label[1] ? file_label[1] : cmp.file[1].name); } else { /* Flush stdout so that the user sees differences immediately. This can hurt performance, unfortunately. */ if (fflush (stdout) != 0) pfatal_with_name (_("standard output")); } free (free0); free (free1); return status; }