Subject: Re: bug#22144: --exclude no longer works against arguments with a directory name

From: Paul Eggert <[email-masked]>
Date: Mon, 28 Dec 2015 01:09:40 -0800

Vincent Lefevre wrote:
> The documentation is still ambiguous. For the "main case", is this
> the canonical name as returned by realpath?
>
>> >As you say, the 2.22 behavior does not seem ideal.
> By doing a difference for subfiles of a recursive search, this is
> even worse!

Please try the attached patch, which I've installed into the savannah 
repository. It attempts to fix the behavior, and to clarify the documentation. 
It is a tricky area. Hope this helps.
>From 7dc95d5dd3965df188533ac1ffef903cf41a11e5 Mon Sep 17 00:00:00 2001
From: Paul Eggert <[email-masked]>
Date: Mon, 28 Dec 2015 01:04:18 -0800
Subject: [PATCH] grep: --exclude matches trailing parts of args

Problem reported by Vincent Lefevre in:
http://bugs.gnu.org/22144
* NEWS:
* doc/grep.texi (File and Directory Selection): Document this.
* src/grep.c (excluded_patterns, excluded_directory_patterns):
Now 2-element arrays, with one element for subfiles and another
for command-line args.  All uses changed.  This implements the change.
(exclude_options): New function.
* tests/include-exclude: Test the change.
---
 NEWS                  |  5 ++++
 doc/grep.texi         | 13 +++++++----
 src/grep.c            | 65 +++++++++++++++++++++++++++++++++------------------
 tests/include-exclude |  4 +++-
 4 files changed, 58 insertions(+), 29 deletions(-)

diff --git a/NEWS b/NEWS
index fc5fd3a..4e54b49 100644
--- a/NEWS
+++ b/NEWS
@@ -8,6 +8,11 @@ GNU grep NEWS                                    -*- outline -*-
   invalid UTF8 just before a match.
   [bug introduced in grep-2.22]
 
+  --exclude and related options are now matched against trailing
+  parts of command-line arguments, not against the entire arguments.
+  This partly reverts the --exclude-related change in 2.22.
+  [bug introduced in grep-2.22]
+
 
 * Noteworthy changes in release 2.22 (2015-11-01) [stable]
 
diff --git a/doc/grep.texi b/doc/grep.texi
index e3495bb..76c7f46 100644
--- a/doc/grep.texi
+++ b/doc/grep.texi
@@ -661,8 +661,10 @@ this is equivalent to the @option{-r} option.
 @opindex --exclude
 @cindex exclude files
 @cindex searching directory trees
-Skip files whose name matches the pattern @var{glob}, using wildcard
-matching.  When searching recursively, skip any subfile whose base
+Skip any command-line file with a name suffix that matches the pattern
+@var{glob}, using wildcard matching; a name suffix is either the whole
+name, or any suffix starting after a @samp{/} and before a
+non-@samp{/}.  When searching recursively, skip any subfile whose base
 name matches @var{glob}; the base name is the part after the last
 @samp{/}.  A pattern can use
 @samp{*}, @samp{?}, and @samp{[}...@samp{]} as wildcards,
@@ -679,9 +681,10 @@ under @option{--exclude}).
 @item --exclude-dir=@var{glob}
 @opindex --exclude-dir
 @cindex exclude directories
-Skip any directory whose name matches the pattern @var{glob}.  When
-searching recursively, skip any subdirectory whose base name matches
-@var{glob}.  Ignore any redundant trailing slashes in @var{glob}.
+Skip any command-line directory with a name suffix that matches the
+pattern @var{glob}.  When searching recursively, skip any subdirectory
+whose base name matches @var{glob}.  Ignore any redundant trailing
+slashes in @var{glob}.
 
 @item -I
 Process a binary file as if it did not contain matching data;
diff --git a/src/grep.c b/src/grep.c
index 2c5e09a..19ba208 100644
--- a/src/grep.c
+++ b/src/grep.c
@@ -296,8 +296,8 @@ static const struct color_cap color_dict[] =
     { NULL, NULL,                  NULL }
   };
 
-static struct exclude *excluded_patterns;
-static struct exclude *excluded_directory_patterns;
+static struct exclude *excluded_patterns[2];
+static struct exclude *excluded_directory_patterns[2];
 /* Short options.  */
 static char const short_options[] =
 "0123456789A:B:C:D:EFGHIPTUVX:abcd:e:f:hiLlm:noqRrsuvwxyZz";
@@ -640,19 +640,30 @@ context_length_arg (char const *str, intmax_t *out)
     }
 }
 
+/* Return the add_exclude options suitable for excluding a file name.
+   If COMMAND_LINE, it is a command-line file name.  */
+static int
+exclude_options (bool command_line)
+{
+  return EXCLUDE_WILDCARDS | (command_line ? 0 : EXCLUDE_ANCHORED);
+}
+
 /* Return true if the file with NAME should be skipped.
    If COMMAND_LINE, it is a command-line argument.
    If IS_DIR, it is a directory.  */
 static bool
 skipped_file (char const *name, bool command_line, bool is_dir)
 {
-  return (is_dir
-          ? (directories == SKIP_DIRECTORIES
-             || (! (command_line && omit_dot_slash)
-                 && excluded_directory_patterns
-                 && excluded_file_name (excluded_directory_patterns, name)))
-          : (excluded_patterns
-             && excluded_file_name (excluded_patterns, name)));
+  struct exclude **pats;
+  if (! is_dir)
+    pats = excluded_patterns;
+  else if (directories == SKIP_DIRECTORIES)
+    return true;
+  else if (command_line && omit_dot_slash)
+    return false;
+  else
+    pats = excluded_directory_patterns;
+  return pats[command_line] && excluded_file_name (pats[command_line], name);
 }
 
 /* Hairy buffering mechanism for grep.  The intent is to keep
@@ -2446,28 +2457,36 @@ main (int argc, char **argv)
 
       case EXCLUDE_OPTION:
       case INCLUDE_OPTION:
-        if (!excluded_patterns)
-          excluded_patterns = new_exclude ();
-        add_exclude (excluded_patterns, optarg,
-                     (EXCLUDE_ANCHORED | EXCLUDE_WILDCARDS
-                      | (opt == INCLUDE_OPTION ? EXCLUDE_INCLUDE : 0)));
+        for (int cmd = 0; cmd < 2; cmd++)
+          {
+            if (!excluded_patterns[cmd])
+              excluded_patterns[cmd] = new_exclude ();
+            add_exclude (excluded_patterns[cmd], optarg,
+                         ((opt == INCLUDE_OPTION ? EXCLUDE_INCLUDE : 0)
+                          | exclude_options (cmd)));
+          }
         break;
       case EXCLUDE_FROM_OPTION:
-        if (!excluded_patterns)
-          excluded_patterns = new_exclude ();
-        if (add_exclude_file (add_exclude, excluded_patterns, optarg,
-                              EXCLUDE_ANCHORED | EXCLUDE_WILDCARDS, '\n') != 0)
+        for (int cmd = 0; cmd < 2; cmd++)
           {
-            error (EXIT_TROUBLE, errno, "%s", optarg);
+            if (!excluded_patterns[cmd])
+              excluded_patterns[cmd] = new_exclude ();
+            if (add_exclude_file (add_exclude, excluded_patterns[cmd],
+                                  optarg, exclude_options (cmd), '\n')
+                != 0)
+              error (EXIT_TROUBLE, errno, "%s", optarg);
           }
         break;
 
       case EXCLUDE_DIRECTORY_OPTION:
-        if (!excluded_directory_patterns)
-          excluded_directory_patterns = new_exclude ();
         strip_trailing_slashes (optarg);
-        add_exclude (excluded_directory_patterns, optarg,
-                     EXCLUDE_ANCHORED | EXCLUDE_WILDCARDS);
+        for (int cmd = 0; cmd < 2; cmd++)
+          {
+            if (!excluded_directory_patterns[cmd])
+              excluded_directory_patterns[cmd] = new_exclude ();
+            add_exclude (excluded_directory_patterns[cmd], optarg,
+                         exclude_options (cmd));
+          }
         break;
 
       case GROUP_SEPARATOR_OPTION:
diff --git a/tests/include-exclude b/tests/include-exclude
index 6a9d269..c3d22a1 100755
--- a/tests/include-exclude
+++ b/tests/include-exclude
@@ -66,9 +66,11 @@ for exclude in 'x' 'x*'; do
     grep -rl --exclude-dir="$exclude" . x > out
     test $? -eq 1 || fail=1
     compare /dev/null out || fail=1
+done
 
+for exclude in 'x' 'x*' './x' './x*'; do
     grep -rl --exclude-dir="$exclude" . ./x | sort > out || fail=1
-    compare exp-dotnames out || fail=1
+    compare /dev/null out || fail=1
 done
 
 Exit $fail
-- 
2.5.0