Here's a description and source of a new diff option to ignore
equivalent strings.
Any comments?
-z ERE --ignore-equivalent-strings=ERE
Treat as equivalent any strings that match an extended regular
expression (ERE), at the same position in both files. (The examples
below should make this a bit clearer). It operates during the
differencing algorithm itself (unlike -I which post-processes the 'diff'
output). The option can be used multiple times (although there are some
caveats explained below), and ERE matching respects the case set by -i.
Note that -z does not directly ignore strings, it is merely treating
them as equivalent. However, it is possible to genuinely ignore strings
if the ERE can match an empty string (e.g. -z 'str|' or -z ' *'). Also
note that the ERE is treated symmetrically between both files (it's not
possible to treat as equivalent some string only in file 1 and some
other string only in file 2). Here are some examples:
-z abc !! Does nothing - don't use it like this !!
This will not affect the 'diff' output! Any string "abc" in file 1 would
be considered equivalent to any string "abc" in file 2. Of course, this
is what would happen anyway, had -z not been used. There is no point in
giving -z a plain string like this.
-z 'unsigned char|char'
The strings "unsigned char" and "char" are considered to be equivalent
to each other. Anywhere "char" appears in a file, if there is a
corresponding string "unsigned char" in the other file, then this
difference is ignored. Of course, "char" in one file still matches
"char" in the other, and the same is true for "unsigned char".
-z 'unsigned char|signed char|char'
The strings "unsigned char", "signed char" and "char" are all considered
to be equivalent to each other.
-z 'const |'
This genuinely ignores all strings "const ". What it does is to consider
any strings "const " to be equivalent to the empty string "". As far as
the differencing is concerned it's as if the string "const " doesn't
exist in the files - the strings will be output of course if the lines
differ for any other reason.
-z '[[:space:]]*'
All contiguous runs of whitespace are treated as equivalent. This
includes runs of length 0. This means that all whitespace is effectively
genuinely ignored. It is equivalent to -w.
-z '[[:space:]]*$' -z '[[:space:]]\+'
All contiguous runs of whitespace at the end of a line are ignored, and
all non-zero contiguous runs of whitespace anywhere in a line are
ignored. It is equivalent to -b.
-z '[0-9]\+\.\?[0-9]*[eE]\?[0-9]*'
Treat (a wide variety of) numbers as equivalent to each other e.g.
3.14E0 27.18E1 789 are all the same.
-z '[0-3]\+[0-9] (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)
2[0-9][0-9][0-9]'
Treat a particular date format as equivalent to each other.
Here are some more details of the algorithm. It has to take the hashing
of each line into account. In this respect it is similar to -E, -b and
-w which treat sequences of whitespace as equivalent by making
appropriate sequences of whitespace contribute identically to the hash
value of each line. Similarly with -z, strings in the text files that
match the same ERE contribute identically to the hash value. Subsequent
to this, 'diff' does a direct comparison of lines from both files which
it judges to be identical (based on the hash) - that logic also has to
take equivalent strings into account.
The matching algorithm is: strings are searched for along each line by
determining the first ERE (in the order they are specified on the
command line) with the next matching starting column. The search then
continues from the column after the end of the string just matched.
<aside>
It was my original intention to match against a different ERE for each
file. Unfortunately this is incompatible with the way diff is coded - it
breaks the concept of equivalent classes. Here's an example, matching
against strings rather than EREs for simplicity. Lets assume that the
notation "-z a=b" means string "a" in file1 is equivalent to string "b"
in file2, and that file1 and file2 consist only of lines containing a
single "a" or "b". Now, a line "a" in file1 goes into equivalence class
1. Then a line "b" also from file1 has to go into a different class,
call it equivalence class 2. But which equivalence class should a line
"b" from file2 go into? It should really go into both since implicitly
"b=b", but by definition "a=b".
Mathematically, for an equivalence relation (~) to hold we must have the
following properties:
Reflexivity: a ~ a
Symmetry: if a ~ b then b ~ a
Transitivity: if a ~ b and b ~ c then a ~ c.
It seems that the second two properties would no longer hold for
something like "-z a=b".
<end aside>
Personally, I find this option extremely useful, as it can remove large
numbers of irrelevant differences, but the user does need to take some
care when specifying multiple EREs. In these cases, if strings matched
by an ERE overlap with strings matched by another ERE (or with any
whitespace characters matched by -E, -b or -w), then the equivalence
assumptions will be broken. If matches could overlap with the match
starting in the same column, then the ERE with the longest match should
normally be placed first. In practise the user should make the EREs as
specific as possible. Some pitfalls of multiple EREs are:
a) -z 'a|b' -z 'a|c'. An "a" in one file would never be equivalent to a
"c" in the other file (-z 'a|b|c' could be used instead - although it's
not quite the same because "b" and "c" are now equivalent to each
other). What happens in the algorithm is that when a character 'a'
occurs in a file, it is hashed similarly to a character 'b' (since this
is the first ERE on the command line). The ERE 'a|c' doesn't get a look
in since ERE 'a|b' takes priority. Even if the first option did not take
priority, we would still have problems since we would loose the
transitivity relation, since b ~ c is not enforced.
b) -z 'abc|uvw' -z 'abcdef|uvwxyz'. "abcdef" and "uvwxyz" would never be
equivalent, since "abcdef" is matched by "abc" first. Switching the ERE
order would work better, because "abc" could not be matched by "abcdef"
(although the strings "abcdef" and "uvwdef" would then be thought
inequivalent).
c) -z '[^c]b' -z 'a[be]'. "ab and "ae" would never be equivalent, since
"ab" is matched by "[^c]b" first.
All these pitfalls have a root cause of multiple EREs being specified
and the strings matching them potentially overlapping. I'm not sure of
the best way to handle this. Disallowing multiple -z's seems a bit
excessive. A way to detect possible overlap of two ERE's matching some
(unspecified) string would be useful, so that a warning could be issued,
but I'm not sure if such an algorithm exists.
Some of the this update is removal of redundant code. In function
find_and_hash_each_line() in io.c IGNORE_TAB_EXPANSION attempts to
account for occurrences of '\b' and '\r'. This coding is redundant
because function lines_differ() in util.c has nothing equivalent. I've
removed the redundant code. Presumably the intention was to ignore
changes that would not be apparent if the files are 'cat'ted, or
something similar - I'm not sure that there's an efficient and general
way to do this.
diff -u -d -x '*.[oea]*' -x '[.M]*' diffutils-3.0/src/diff.c
diffutils-3.0.ZZZ/src/diff.c
--- diffutils-3.0/src/diff.c 2010-04-15 20:53:08.000000000 +0100
+++ diffutils-3.0.ZZZ/src/diff.c 2010-09-27 11:14:46.906250000 +0100
@@ -64,6 +64,8 @@
};
static int compare_files (struct comparison const *, char const *, char const
*);
+static void add_ignore_string (struct ignore_strings *, char *);
+static void compile_ignore_string (struct ignore_strings *);
static void add_regexp (struct regexp_list *, char const *);
static void summarize_regexp_list (struct regexp_list *);
static void specify_style (enum output_style);
@@ -106,7 +108,7 @@
static bool report_identical_files;
static char const shortopts[] =
-"0123456789abBcC:dD:eEfF:hHiI:lL:nNpPqrsS:tTuU:vwW:x:X:y";
+"0123456789abBcC:dD:eEfF:hHiI:lL:nNpPqrsS:tTuU:vwW:x:X:yz:";
/* Values for long options that do not have single-letter equivalents. */
enum
@@ -174,6 +176,7 @@
{"ignore-blank-lines", 0, 0, 'B'},
{"ignore-case", 0, 0, 'i'},
{"ignore-file-name-case", 0, 0, IGNORE_FILE_NAME_CASE_OPTION},
+ {"ignore-equivalent-strings", 1, 0, 'z'},
{"ignore-matching-lines", 1, 0, 'I'},
{"ignore-space-change", 0, 0, 'b'},
{"ignore-tab-expansion", 0, 0, 'E'},
@@ -281,6 +284,8 @@
c_stack_action (0);
function_regexp_list.buf = &function_regexp;
ignore_regexp_list.buf = &ignore_regexp;
+ ignore_string_list.count = 0;
+ ignore_string_list.alloc = 0;
re_set_syntax (RE_SYNTAX_GREP | RE_NO_POSIX_BACKTRACKING);
excluded = new_exclude ();
@@ -514,6 +519,10 @@
}
break;
+ case 'z':
+ add_ignore_string (&ignore_string_list, optarg);
+ break;
+
case BINARY_OPTION:
#if O_BINARY
binary = true;
@@ -659,7 +668,6 @@
tabsize = 8;
if (! width)
width = 130;
-
{
/* Maximize first the half line width, and then the gutter width,
according to the following constraints:
@@ -683,6 +691,7 @@
if (horizon_lines < context)
horizon_lines = context;
+ compile_ignore_string (&ignore_string_list);
summarize_regexp_list (&function_regexp_list);
summarize_regexp_list (&ignore_regexp_list);
@@ -714,7 +723,8 @@
files_can_be_treated_as_binary =
(brief & binary
& ~ (ignore_blank_lines | ignore_case | strip_trailing_cr
- | (ignore_regexp_list.regexps || ignore_white_space)));
+ | (ignore_regexp_list.regexps || ignore_white_space ||
+ ignore_string_list.count)));
switch_string = option_list (argv + 1, optind - 1);
@@ -761,6 +771,58 @@
return exit_status;
}
+/* Allocate space and add to IGNLIST the regexps for
+ ignoring strings specified by PATTERN. */
+
+static void
+add_ignore_string (struct ignore_strings *ignlist, char *pattern)
+{
+ /* Allocate space for data structures. */
+ if (ignlist->count <= ignlist->alloc)
+ ignlist->regexp = (struct ignore_string *) x2nrealloc (ignlist->regexp,
+ &ignlist->alloc, sizeof (struct ignore_string));
+
+ size_t i = ignlist->count;
+ ignlist->regexp[i].regs = (struct re_registers *)
+ xmalloc (sizeof (struct re_registers));
+ ignlist->regexp[i].buf = (struct re_pattern_buffer *)
+ xcalloc (1, sizeof (struct re_pattern_buffer));
+ ignlist->regexp[i].buf->fastmap = xmalloc (1 << CHAR_BIT);
+
+ /* Save the pattern, rather than compile it here,
+ since IGNORE_CASE may change. */
+ ignlist->regexp[i].pattern = pattern;
+ ignlist->count++;
+}
+
+/* Compile the regexps in IGNLIST. */
+
+static void
+compile_ignore_string (struct ignore_strings *ignlist)
+{
+ if (!ignlist->count) return;
+
+ reg_syntax_t toggle_re_syntax = re_set_syntax (re_syntax_options |
+ RE_SYNTAX_EGREP | (ignore_case ? RE_ICASE : 0));
+
+ size_t i;
+ for (i = 0; i < ignlist->count; i++)
+ {
+ size_t patlen = strlen (ignlist->regexp[i].pattern);
+ char const *m = re_compile_pattern (ignlist->regexp[i].pattern,
+ patlen, ignlist->regexp[i].buf);
+ if (m != 0)
+ error (0, 0, "%s: %s", ignlist->regexp[i].pattern, m);
+
+ /* Any regexp that can match a NULL string (e.g. -z a|)
+ must not update the hash value. */
+ ignlist->regexp[i].update_hash =
+ re_search (ignlist->regexp[i].buf, "", 0, 0, 0, 0) == 0 ? false : true;
+ }
+
+ toggle_re_syntax=re_set_syntax (toggle_re_syntax);
+}
+
/* Append to REGLIST the regexp PATTERN. */
static void
@@ -849,6 +911,8 @@
N_("-E --ignore-tab-expansion Ignore changes due to tab expansion."),
N_("-b --ignore-space-change Ignore changes in the amount of white
space."),
N_("-w --ignore-all-space Ignore all white space."),
+ N_("-z ERE --ignore-equivalent-strings=ERE"),
+ N_(" Ignore strings matching ERE in both
files."),
N_("-B --ignore-blank-lines Ignore changes whose lines are all blank."),
N_("-I RE --ignore-matching-lines=RE Ignore changes whose lines all match
RE."),
N_("--strip-trailing-cr Strip trailing carriage return on input."),
@@ -1310,7 +1374,7 @@
if (status == EXIT_SUCCESS)
{
- if (report_identical_files && !DIR_P (0))
+ if (report_identical_files && !DIR_P (0) && !DIR_P (1))
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);
diff -u -d -x '*.[oea]*' -x '[.M]*' diffutils-3.0/src/diff.h
diffutils-3.0.ZZZ/src/diff.h
--- diffutils-3.0/src/diff.h 2010-04-15 20:53:08.000000000 +0100
+++ diffutils-3.0.ZZZ/src/diff.h 2010-09-27 11:14:46.906250000 +0100
@@ -135,6 +135,26 @@
/* Ignore changes that affect only lines matching this regexp (-I). */
XTERN struct re_pattern_buffer ignore_regexp;
+/* Ignore strings that match a regexp in both files (-z). */
+struct ignore_string
+{
+ struct re_pattern_buffer *buf;
+ struct re_registers *regs;
+ char *pattern;
+ int begin1;
+ int begin2;
+ int size1;
+ int size2;
+ bool update_hash;
+};
+struct ignore_strings
+{
+ struct ignore_string *regexp;
+ size_t alloc;
+ size_t count;
+};
+XTERN struct ignore_strings ignore_string_list;
+
/* Say only whether files differ, not how (-q). */
XTERN bool brief;
@@ -350,7 +370,7 @@
extern char const pr_program[];
char *concat (char const *, char const *, char const *);
char *dir_file_pathname (char const *, char const *);
-bool lines_differ (char const *, char const *);
+bool lines_differ (char const *, size_t, char const *, size_t);
lin translate_line_number (struct file_data const *, lin);
struct change *find_change (struct change *);
struct change *find_reverse_change (struct change *);
diff -u -d -x '*.[oea]*' -x '[.M]*' diffutils-3.0/src/io.c
diffutils-3.0.ZZZ/src/io.c
--- diffutils-3.0/src/io.c 2010-04-17 07:15:46.000000000 +0100
+++ diffutils-3.0.ZZZ/src/io.c 2010-09-27 11:14:46.906250000 +0100
@@ -216,7 +216,7 @@
char const *suffix_begin = current->suffix_begin;
char const *bufend = FILE_BUFFER (current) + current->buffered;
bool diff_length_compare_anyway =
- ignore_white_space != IGNORE_NO_WHITE_SPACE;
+ ignore_white_space != IGNORE_NO_WHITE_SPACE || ignore_string_list.count;
bool same_length_diff_contents_compare_anyway =
diff_length_compare_anyway | ignore_case;
@@ -227,6 +227,7 @@
h = 0;
/* Hash this line until we find a newline. */
+ if (!ignore_string_list.count)
if (ignore_case)
switch (ignore_white_space)
{
@@ -263,20 +264,10 @@
switch (c)
{
- case '\b':
- column -= 0 < column;
- break;
-
case '\t':
c = ' ';
repetitions = tabsize - column % tabsize;
- column = (column + repetitions < column
- ? 0
- : column + repetitions);
- break;
-
- case '\r':
- column = 0;
+ column += repetitions;
break;
default:
@@ -333,20 +324,10 @@
switch (c)
{
- case '\b':
- column -= 0 < column;
- break;
-
case '\t':
c = ' ';
repetitions = tabsize - column % tabsize;
- column = (column + repetitions < column
- ? 0
- : column + repetitions);
- break;
-
- case '\r':
- column = 0;
+ column += repetitions;
break;
default:
@@ -366,6 +347,116 @@
h = HASH (h, c);
break;
}
+ else
+ {
+ size_t len = (size_t) strchr (p,'\n') - (size_t) p;
+ size_t column = 0;
+ size_t i;
+ for (i = 0 ; i < ignore_string_list.count; i++)
+ ignore_string_list.regexp[i].begin1 = -1;
+
+ while (1)
+ {
+ start_loop :
+ for (i = 0; i < ignore_string_list.count; i++)
+ {
+ struct ignore_string *ign = &ignore_string_list.regexp[i];
+ if (ign->begin1 == -2) continue;
+ if (ip + ign->begin1 < p)
+ {
+ if ((ign->begin1 = re_search (ign->buf, ip, len, p - ip,
len, ign->regs)) < 0)
+ ign->begin1 = -2;
+ else
+ ign->size1 = ign->regs->end[0] - ign->regs->start[0];
+ }
+ if (ip + ign->begin1 == p)
+ {
+ ign->begin1 = -1;
+ if (ign->size1 == 0)
+ continue; /* Proceed to the next regexp. */
+ /* Update the hash value if the regexps
+ can only match non-NULL strings. */
+ if (ignore_string_list.regexp[i].update_hash)
+ {
+ char* pp;
+ for (pp = ign->pattern; *pp; pp++)
+ h = HASH (h, *pp);
+ }
+ p += ign->size1;
+ column += ign->size1;
+ goto start_loop;
+ }
+ }
+
+ c = *p++;
+ char const *pp = p;
+
+ switch (ignore_white_space)
+ {
+ case IGNORE_ALL_SPACE:
+ /* For -w, just skip past any white space. */
+ while (isspace (c) && c != '\n')
+ c = *p++;
+ break;
+
+ case IGNORE_SPACE_CHANGE:
+ /* For -b, advance past any sequence of white space
+ and consider it just one space, or nothing at
+ all if it is at the end of the line. */
+ if (isspace (c))
+ {
+ while (c != '\n')
+ {
+ c = *p++;
+ if (! isspace (c))
+ {
+ h = HASH (h, ' ');
+ break;
+ }
+ }
+ }
+ break;
+
+ case IGNORE_TAB_EXPANSION:
+ {
+ if (c == ' ' || c == '\t')
+ {
+ for (;; c = *p++)
+ {
+ if (c == ' ')
+ column++;
+ else if (c == '\t')
+ column += tabsize - column % tabsize;
+ else
+ break;
+ }
+ /* With the -z option, we may need different 'lengths'
of
+ white space to realign to the same column. Therefore
we cannot
+ hash the whole length - so just use one space. */
+ h = HASH (h, ' ');
+ }
+ }
+ break;
+
+ case IGNORE_NO_WHITE_SPACE:
+ break;
+ }
+
+ if (p != pp)
+ {
+ --p;
+ continue;
+ }
+
+ if (c != '\n')
+ {
+ column++;
+ h = HASH (h, ignore_case ? tolower (c) : c);
+ }
+ else
+ break;
+ }
+ }
hashing_done:;
@@ -423,7 +514,7 @@
else if (!diff_length_compare_anyway)
continue;
- if (! lines_differ (eqline, ip))
+ if (! lines_differ (eqline, eqs[i].length, ip, length))
break;
}
Only in diffutils-3.0.ZZZ/src: paths.h
diff -u -d -x '*.[oea]*' -x '[.M]*' diffutils-3.0/src/util.c
diffutils-3.0.ZZZ/src/util.c
--- diffutils-3.0/src/util.c 2010-04-15 20:53:08.000000000 +0100
+++ diffutils-3.0.ZZZ/src/util.c 2010-09-27 11:14:46.906250000 +0100
@@ -317,16 +317,61 @@
Return nonzero if the lines differ. */
bool
-lines_differ (char const *s1, char const *s2)
+lines_differ (char const *s1, size_t len1, char const *s2, size_t len2)
{
register char const *t1 = s1;
register char const *t2 = s2;
- size_t column = 0;
+ size_t i;
+ for (i = 0; i < ignore_string_list.count; i++) {
+ ignore_string_list.regexp[i].begin1 = -1;
+ ignore_string_list.regexp[i].begin2 = -1;
+ }
+ size_t column1 = 0;
+ size_t column2 = 0;
while (1)
{
+ start_loop :
+ for (i = 0; i < ignore_string_list.count; i++)
+ {
+ struct ignore_string *ign = &ignore_string_list.regexp[i];
+ if (ign->begin1 == -2 || ign->begin2 == -2) continue;
+
+ if (s1 + ign->begin1 < t1)
+ {
+ if ((ign->begin1 = re_search (ign->buf, s1, len1, t1 - s1, len1,
ign->regs)) < 0)
+ ign->begin1 = -2;
+ else
+ ign->size1 = ign->regs->end[0] - ign->regs->start[0];
+ }
+ if (s1 + ign->begin1 == t1)
+ ign->begin1 = -1;
+
+ if (s2 + ign->begin2 < t2)
+ {
+ if ((ign->begin2 = re_search (ign->buf, s2, len2, t2 - s2, len2,
ign->regs)) <0 )
+ ign->begin2 = -2;
+ else
+ ign->size2 = ign->regs->end[0] - ign->regs->start[0];
+ }
+ if (s2 + ign->begin2 == t2)
+ ign->begin2 = -1;
+
+ if (ign->begin1 == -1 && ign->begin2 == -1)
+ {
+ if (ign->size1 + ign->size2 == 0)
+ continue; /* Proceed to the next regexp, if matched strings are
both empty. */
+ t1 += ign->size1;
+ t2 += ign->size2;
+ column1 += ign->size1;
+ column2 += ign->size2;
+ goto start_loop;
+ }
+ }
register unsigned char c1 = *t1++;
register unsigned char c2 = *t2++;
+ char const *tt1 = t1;
+ char const *tt2 = t2;
/* Test for exact char equality first, since it's a common case. */
if (c1 != c2)
@@ -399,13 +444,12 @@
if ((c1 == ' ' && c2 == '\t')
|| (c1 == '\t' && c2 == ' '))
{
- size_t column2 = column;
for (;; c1 = *t1++)
{
if (c1 == ' ')
- column++;
+ column1++;
else if (c1 == '\t')
- column += tabsize - column % tabsize;
+ column1 += tabsize - column1 % tabsize;
else
break;
}
@@ -418,7 +462,7 @@
else
break;
}
- if (column != column2)
+ if (column1 != column2)
return true;
}
break;
@@ -426,6 +470,10 @@
case IGNORE_NO_WHITE_SPACE:
break;
}
+ if (ignore_string_list.count&&(t1!=tt1||t2!=tt2)) {
+ --t1;--t2;
+ continue;
+ }
/* Lowercase all letters if -i is specified. */
@@ -441,7 +489,8 @@
if (c1 == '\n')
return false;
- column += c1 == '\t' ? tabsize - column % tabsize : 1;
+ column1 += c1 == '\t' ? tabsize - column1 % tabsize : 1;
+ column2 += c2 == '\t' ? tabsize - column2 % tabsize : 1;
}
return true;