3

Let's say I have this files:

xb@dnxb:/tmp/aaa/webview$ tree -F -C -a .
.
├── .git/
├── ss/
├── y
└── !yes/

3 directories, 1 file
xb@dnxb:/tmp/aaa/webview$ 

Negate the !yes is working fine by escape the ! with \:

xb@dnxb:/tmp/aaa/webview$ mkdir /tmp/aaa/webview2; cp -r -a !(\!yes) /tmp/aaa/webview2                                         
xb@dnxb:/tmp/aaa/webview$ l /tmp/aaa/webview2
total 16K
39331773 -rw-rw-r-- 1 xiaobai xiaobai ?    0 Jul  27 05:07 y
39331771 drwxrwxr-x 2 xiaobai xiaobai ? 4.0K Jul  27 05:43 .git/
39331772 drwxrwxr-x 2 xiaobai xiaobai ? 4.0K Jul  27 05:43 ss/
39331757 drwxrwxr-x 4 xiaobai xiaobai ? 4.0K Jul  27 05:44 ../
39331770 drwxrwxr-x 4 xiaobai xiaobai ? 4.0K Jul  27 05:44 ./
xb@dnxb:/tmp/aaa/webview$

But !(.git) not working:

xb@dnxb:/tmp/test/hello$ rm -r /tmp/test; shopt -s extglob; shopt -s dotglob; mkdir -p /tmp/test/hello; mkdir /tmp/test/hello2; cd /tmp/test/hello; mkdir '.git'; cp -r -a !(.git) /tmp/test/hello2/; ls -la /tmp/test/hello2/
cp: will not create hard link '/tmp/test/hello2/hello' to directory '/tmp/test/hello2/.'
cp: cannot copy a directory, '..', into itself, '/tmp/test/hello2/'
total 16
drwxrwxr-x 4 xiaobai xiaobai 4096 Jul  27 16:05 .
drwxrwxr-x 4 xiaobai xiaobai 4096 Jul  27 16:05 ..
drwxrwxr-x 2 xiaobai xiaobai 4096 Jul  27 16:05 .git
drwxrwxr-x 2 xiaobai xiaobai 4096 Jul  27 16:05 hello2
xb@dnxb:/tmp/test/hello$ 

Escape . with \ not working:

xb@dnxb:/tmp/aaa/webview$ rm -r /tmp/aaa/webview2
xb@dnxb:/tmp/aaa/webview$ mkdir /tmp/aaa/webview2; cp -r -a !(\.git) /tmp/aaa/webview2        
cp: will not create hard link '/tmp/aaa/webview2/webview' to directory '/tmp/aaa/webview2/.'
cp: cannot copy a directory, '..', into itself, '/tmp/aaa/webview2'
xb@dnxb:/tmp/aaa/webview$ l /tmp/aaa/webview2                                         
total 24K
39331773 -rw-rw-r-- 1 xiaobai xiaobai ?    0 Jul  27 05:07 y
39331774 drwxrwxr-x 2 xiaobai xiaobai ? 4.0K Jul  27 05:39 !yes/
39331775 drwxrwxr-x 2 xiaobai xiaobai ? 4.0K Jul  27 05:39 webview2/
39331771 drwxrwxr-x 2 xiaobai xiaobai ? 4.0K Jul  27 05:43 .git/
39331772 drwxrwxr-x 2 xiaobai xiaobai ? 4.0K Jul  27 05:43 ss/
39331757 drwxrwxr-x 4 xiaobai xiaobai ? 4.0K Jul  27 05:44 ../
39331770 drwxrwxr-x 6 xiaobai xiaobai ? 4.0K Jul  27 05:44 ./
xb@dnxb:/tmp/aaa/webview$

Double slash \\ not working either:

xb@dnxb:/tmp/aaa/webview$ rm -r /tmp/aaa/webview2
xb@dnxb:/tmp/aaa/webview$ mkdir /tmp/aaa/webview2; cp -r -a !(\\.git) /tmp/aaa/webview2
xb@dnxb:/tmp/aaa/webview$ l /tmp/aaa/webview2
total 20K
39331773 -rw-rw-r-- 1 xiaobai xiaobai ?    0 Jul  27 05:07 y
39331774 drwxrwxr-x 2 xiaobai xiaobai ? 4.0K Jul  27 05:39 !yes/
39331771 drwxrwxr-x 2 xiaobai xiaobai ? 4.0K Jul  27 05:43 .git/
39331772 drwxrwxr-x 2 xiaobai xiaobai ? 4.0K Jul  27 05:43 ss/
39331757 drwxrwxr-x 4 xiaobai xiaobai ? 4.0K Jul  27 05:44 ../
39331770 drwxrwxr-x 5 xiaobai xiaobai ? 4.0K Jul  27 05:44 ./
xb@dnxb:/tmp/aaa/webview$ 

And I found this working:

xb@dnxb:/tmp/aaa/webview$ rm -r /tmp/aaa/webview2
xb@dnxb:/tmp/aaa/webview$ mkdir /tmp/aaa/webview2; cp -r -a !(.git|.|..) /tmp/aaa/webview2
xb@dnxb:/tmp/aaa/webview$ l /tmp/aaa/webview2
total 16K
39331772 -rw-rw-r-- 1 xiaobai xiaobai ?    0 Jul  27 05:07 y
39331773 drwxrwxr-x 2 xiaobai xiaobai ? 4.0K Jul  27 05:39 !yes/
39331771 drwxrwxr-x 2 xiaobai xiaobai ? 4.0K Jul  27 05:43 ss/
39331757 drwxrwxr-x 4 xiaobai xiaobai ? 4.0K Jul  27 05:45 ../
39331770 drwxrwxr-x 4 xiaobai xiaobai ? 4.0K Jul  27 05:45 ./
xb@dnxb:/tmp/aaa/webview$

Why direct escape dot not working ? And what's the proper way to negate in this case ? (I doubt !(.git|.|..) will get unexpected result in some cases).

Note that I know rsync can be used to exclude, but it's not my question.

[UPDATE]

I did enabled extglob and dotglob for my test case, they're not the cause.

I think the reason of !(.git) failed is because of . and .. error and abort, not because of .git expansion was failed. And !(.git|.|..) should be the correct way already.

  • Might be worth saying that you've got shopt -s extglob enabled for the ! negation operator to be recognised. None of my bash shells has that enabled by default. – roaima Jul 26 '17 at 22:45
  • @roaima yes, I've enabled extglob. – 林果皞 Jul 26 '17 at 22:54
  • 1
    I've edited your question to make it clearer what you're asking; 3 people had voted to close this for being unclear but I figured a more descriptive title should be enough to clarify the intent. – Anthony Geoghegan Jul 27 '17 at 8:54
2

In bash, without the dotglob option, hidden files are ignored unless the glob explicitly (that is with a literal leading .) asks for them. And . and .. are not ignored (contrary to what more sensible shells like pdksh, zsh or fish do).

With dotglob, only . and .. are ignored unless the glob starts with ..

With extglob, bash adds support for some of ksh extended glob operators. However, here with a bug/misfeature when it comes to the !(...) one.

In both ksh (though not pdksh) and bash, @(.*) is one case where you explicitly request dotfiles (though the documentation doesn't make that clear).

But in !(.git) you're not requesting dotfiles. ksh and zsh in ksh emulation handle it correctly, but bash presumes you're requesting dotfiles. And that includes . and .. even with dotglob. So .git will be copied as part of the copying of ..

To work around that, you can use !([.]git) (here [.] makes the . not explicit) in combination with dotglob, or exclude . and .. explicitly with !(.git|.|..):

$ bash -O extglob -c 'echo !(.git)'
. .. foo .foo
$ bash -O extglob -O dotglob -c 'echo !(.git)'
. .. foo .foo
$ bash -O extglob -O dotglob -c 'echo !([.]git)'
foo .foo
$ bash -O extglob -c 'echo !(.git|.|..)'
foo .foo

In the latter case, I would still add the dotglob option because bash in a future version may be fixed to stop including dotfiles here like in other shells.

As I use zsh which has its own extended operators (with the extendedglob option not enabled by default to keep Bourne compatibility; it also supports ksh globs with the kshglob option but those are more awkward), I'd do:

set -o extendedglob # (in my ~/.zshrc)
cp -a -- ^.git(D) target/

(note that -a implies -r and you need the -- as we can't guarantee file names won't start with -).

^.git is zsh's equivalent of ksh's !(.git). (D) to include dotfiles (but never . nor ..) for that glob only.

  • Thanks, is there any documentation mentioned about [.] makes the . not explicit ? – 林果皞 Jul 27 '17 at 10:39
  • 1
    @林果皞, the doc says that . must be named explicitly. ?, *, [^x], [.] match a dot but are not an explicit dot. Still @(.) is not . either, but @(.)* still matches dotfiles, so the documentation is incomplete on that front. – Stéphane Chazelas Jul 27 '17 at 10:51
2

By default, the shell don't include '.' in expansion of wildcards. Run shopt -s dotglob which enables the shell expansion to include '.'

dotglob
    If set, Bash includes filenames beginning with a ‘.’ in the results of 
    filename expansion.

Shopt builtin man page

  • I've enabled dotglob but .git still exist in the destination directory. – 林果皞 Jul 26 '17 at 23:16
  • I've tested it with cp -r -a !(.filename) folder and it works as expected on bash 4.2. On what OS are you running it? and what is the bash version you use? – Chen A. Jul 27 '17 at 7:37
  • Don't you still see .git when test with shopt -s extglob; shopt -s dotglob; mkdir -p /tmp/test/hello; mkdir /tmp/test/hello2; cd /tmp/test/hello; mkdir '.git'; cp -r -a !(.git) /tmp/test/hello2/; ls -la /tmp/test/hello2/ ? Bash version is GNU bash, version 4.4.7(1)-release (x86_64-pc-linux-gnu) in ubuntu 17.04. I'm curious why you don't encounter .. error. – 林果皞 Jul 27 '17 at 7:45
  • Oh I see, you should put .git in '.git', this would work. – Chen A. Jul 27 '17 at 8:12
  • !('.git') not working. – 林果皞 Jul 27 '17 at 8:28

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.