Bash extglob: Should the order of patterns in a pattern list matter

bashwildcards

According to the bash manual, if extglob is enabled,
the pattern @(pattern-list) should match any of the patterns in pattern-list (separated by |). Here it works as expected:

$ shopt -s extglob
$ ls -ld /@(.|usr)/@(.|local)/@(.|share)/
drwxr-xr-x  50 root root   4096 Sep  2 16:39 /./././
drwxr-xr-x  12 root root   4096 Oct 15  2018 /usr/././
drwxrwsr-x  10 root staff  4096 Oct 15  2018 /usr/local/./
drwxrwsr-x  10 root staff  4096 Oct 15  2018 /usr/local/share/
drwxr-xr-x 725 root root  20480 Sep  2 16:42 /usr/./share/

But if we swap the alternatives in each of the three pattern lists,
most of the directories that should have been matched are gone:

$ ls -ld /@(usr|.)/@(local|.)/@(share|.)/
drwxrwsr-x 10 root staff 4096 Oct 15  2018 /usr/local/share/

The same with a non-existing subdirectory. Here it works:

$ ls -ld /@(.|usr)/@(.|foo)/@(.|share)/
drwxr-xr-x  50 root root  4096 Sep  2 16:39 /./././
drwxr-xr-x  12 root root  4096 Oct 15  2018 /usr/././
drwxr-xr-x 725 root root 20480 Sep  2 16:42 /usr/./share/

And here it doesn't:

$ ls -ld /@(usr|.)/@(foo|.)/@(share|.)/
ls: cannot access '/@(usr|.)/@(foo|.)/@(share|.)/': No such file or directory

What's going on here? Is this behavior documented somewhere, or is it just plain buggy? (This is GNU bash, version 4.4.12(1).)

Best Answer

Prior to bash-4.3, the "." term would never match. From bash(1), v5.0, section Pathname Expansion:

                                                When a  pattern  is  used
  for  pathname expansion, the character ``.''  at the start of a name or
  immediately following a slash must be matched  explicitly,  unless  the
  shell  option  dotglob  is  set.  The filenames ``.''  and ``..''  must
  always be matched explicitly, even if dotglob is set.

The description of the behaviour here is a little vague, but it doesn't mean that "." must be at the start of each (sub-)pattern, you can prove that with:

$ echo  /@(usr|.)/@(local|.)/@(share|.)/
/usr/local/share/
$ echo  /@(usr|..)/@(local|..)/@(share|..)/
/../../../ /usr/../../ /usr/local/../ /usr/local/share/

So the problem is specific to "." and not "..".

I believe this is a bug in extglob_skipname(), starting at line 218 in the while (t = glob_patscan (pp, pe, '|')) { ... } loop, the final term in such a pattern is not handled properly (interaction with the leading "." supression logic in skipname()) so "." never matches but ".." manages to match. (glob_patscan is aka PATSCAN thanks to macro games.)

Either of these work too:

$ echo  /@(usr|.|)/@(local|.|)/@(share|.|)/
/./././ /usr/././ /usr/./share/ /usr/local/./ /usr/local/share/
$ echo  /@(usr|.|.)/@(local|.|.)/@(share|.|.)/
/./././ /usr/././ /usr/./share/ /usr/local/./ /usr/local/share/

So the answer is the sub-pattern order shouldn't matter, and doesn't matter, but seemingly a bug causes problems when the final term is a ".".

Related Question