The enclosed patch fixes (or tries to fix) the inability to glob for executable files on cygwin without the ".exe" suffix. This is required because cygwin has a hack to behave as though foo and foo.exe both exist as hardlinks to the same file, but unlike a real hard-link, readdir() only provides the ".exe"-suffixed filename.

I posted this to the cygwin mailing list some days ago and nobody's ridiculed or flamed me yet, which comes about as close to a ringing endorsement as I would dare to hope for :)

In all seriousness, I doubt it's 100% correct. There's a lot of goofy corner cases I can imagine, i.e.: what happens if foo.exe is renamed to foo outside of cygwin? So far, I haven't gone out of my way to try and test all of these possibilities -- the patch only deals with the typical case where readdir() returns foo.exe and we are globbing for foo.

Assuming there's no memory allocation bugs or similar lurking in the patch, at worst, it makes bash globbing less broken on cygwin, even if it leaves more work to be done to make it fully correct.

I'd be happy to iterate if folks have some good ideas as to how to improve the implementation.

-gmt
commit e6206e337ac4577e4b83ceaac44241f48a2aec50
Author: Gregory M. Turner <gmturner...@ameritech.net>
Date:   Sat Sep 8 22:05:06 2012 -0700

    lib/glob: cygwin: match executables without ".exe" suffix
    
    On cygwin we have the ".exe-hack" which is a feature that
    attempts to make the Windowsy ".exe" suffix on executables
    optional by acting (almost) as though "foo" and "foo.exe" were
    hard-links to the same file.
    
    The "almost" part is that when enumarating a directory's
    contents, only the ".exe"-suffixed version is returned.
    As a result, bash has historically failed to match globs
    which don't match the ".exe" suffix, even though, for all
    intents and purposes, the file is there.  For example,
    
      echo /bin/ba?h
    
    would not find the /bin/bash executable.
    
    This patch fixes this by keeping an eye out for files that fail to
    match the glob but do look like they'd match "*?.exe".
    
    When such a file is encountered, some sanity checks are
    performed, and if the ".exe-hack" seems applicable, the
    ".exe" suffix is removed and the glob is checked against
    the suffix-free version of the filename, before rejection.
    Such matches are returned without the .exe suffix (as, it
    would be quite unexpected to get a result back from globbing
    that doesn't match the glob!).
    
    Signed-off-by: Gregory M. Turner <gmturner...@ameritech.net>

diff --git a/lib/glob/glob.c b/lib/glob/glob.c
index ad9b9d9..fd90e7d 100644
--- a/lib/glob/glob.c
+++ b/lib/glob/glob.c
@@ -675,6 +675,109 @@ glob_vector (pat, dir, flags)
              bcopy (dp->d_name, nextname, D_NAMLEN (dp) + 1);
              ++count;
            }
+#if __CYGWIN__
+         /* The master plan here is to check whether "foo" matches the glob 
when the
+            above has just ruled out a match for the executable or 
symlink-to-executable
+            "foo.exe".  If it does match, we add "foo" as though we got it 
from readdir.
+
+            Before we leap, though, we should look a bit.  Incorrectly 
guessing whether
+            the .exe hack applies would result in missed globs, or (if we 
inject "mccoy"
+            but then encounter the real "mccoy") duplicate matches; hopefully 
this is
+            never wrong in practice -- if it becomes an issue, we should 
adjust our
+            sanity checking to better capture the conditions actually used by 
cygwin. */
+
+         /* We aren't interested in anything less than five characters long 
(what are
+            we going to match for ".exe," an empty string?) */
+         else if (convfn[0] != '\0' && convfn[1] != '\0' && convfn[2] != '\0' 
&&
+                  convfn[3] != '\0' && convfn[4] != '\0')
+           {
+             register char *x = &convfn[0];
+             struct stat finfo;
+             while (*++x != '\0') ;
+             if (*--x == 'e' && *--x == 'x' && *--x == 'e' && *--x == '.')
+               {
+                 /* abuse the available register char* 'subdir' to build the 
path */
+                 subdir = sh_makepath (dir, dp->d_name, pflags);
+                 if (!subdir)
+                   {
+                     lose = 1;
+                     break;
+                   }
+                 /* My tests indicate that we can stat() the executable bit of 
a valid
+                    symlink in cygwin, expecting to get the same result we 
would get
+                    stat()ing the link-target.  The .exe hack does apply to 
such links! */
+                 if (stat (subdir, &finfo) == 0 &&
+                     ((S_ISREG (finfo.st_mode) || S_ISLNK (finfo.st_mode)) &&
+                      (finfo.st_mode & (S_IXUSR|S_IXGRP|S_IXOTH))))
+                   {
+                     /* So, it's an executable or symlink thereto and ends in 
.exe -- does
+                        it match the pattern if we strip the ".exe" suffix? */
+                     size_t dnamlen = x - convfn; /* x points to the last char 
before '.exe' */
+                     nextname = (char *) malloc (dnamlen + 1);
+                     if (!nextname)
+                       {
+                         lose = 1;
+                         break;
+                       }
+                     bcopy (dp->d_name, nextname, dnamlen);   /* always a noop 
on cygwin */
+                     *(nextname + dnamlen) = '\0';
+                     convfn = fnx_fromfs (nextname, dnamlen);
+                     if (strmatch (pat, convfn, mflags) != FNM_NOMATCH)
+                       {
+                         /* Great, it matches, but does it actually exist, and 
is it executable?
+                            Probably, yes.  This protects against some future 
cygwin that made
+                            the .exe hack optional or removed it, and against 
the possibility that
+                            I've failed to capture the conditions that trigger 
the .exe hack in
+                            general.  A better test might be to check if they 
have the same inode,
+                            but this might require some nuance due to 
inode-reliability quirks */
+                         free(subdir);
+                         subdir = sh_makepath (dir, convfn, pflags);
+                         if (!subdir)
+                           {
+                             lose = 1;
+                             break;
+                           }
+                         if (stat (subdir, &finfo) == 0 &&
+                             ((S_ISREG (finfo.st_mode) || S_ISLNK 
(finfo.st_mode)) &&
+                              (finfo.st_mode & (S_IXUSR|S_IXGRP|S_IXOTH))))
+                           {
+                             /* All signs point to cygwin .exe hack.  Treat it 
as a match */
+                             if (nalloca < ALLOCA_MAX)
+                               {
+                                 nextlink = (struct globval *) alloca (sizeof 
(struct globval));
+                                 nalloca += sizeof (struct globval);
+                               }
+                             else
+                               {
+                                 nextlink = (struct globval *) malloc (sizeof 
(struct globval));
+                                 if (firstmalloc == 0)
+                                 firstmalloc = nextlink;
+                               }
+
+                             if (!nextlink)
+                               {
+                                 lose = 1;
+                                 break;
+                               }
+                             nextlink->next = lastlink;
+                             lastlink = nextlink;
+                             nextlink->name = nextname;
+                             ++count;
+                           }
+                         else
+                           {
+                             free(nextname);
+                           }
+                       }
+                     else
+                       {
+                         free (nextname);
+                       }
+                   }
+                 free(subdir);
+               }
+           }
+#endif
        }
 
       (void) closedir (d);

Reply via email to