382

Say I want to copy the contents of a directory excluding files and folders whose names contain the word 'Music'.

cp [exclude-matches] *Music* /target_directory

What should go in place of [exclude-matches] to accomplish this?

jmlane
  • 2,109
  • 1
  • 16
  • 24
user4812
  • 5,942
  • 9
  • 30
  • 35

11 Answers11

430

In Bash you can do it by enabling the extglob option, like this (replace ls with cp and add the target directory, of course)

~/foobar> shopt extglob
extglob        off
~/foobar> ls
abar  afoo  bbar  bfoo
~/foobar> ls !(b*)
-bash: !: event not found
~/foobar> shopt -s extglob  # Enables extglob
~/foobar> ls !(b*)
abar  afoo
~/foobar> ls !(a*)
bbar  bfoo
~/foobar> ls !(*foo)
abar  bbar

You can later disable extglob with

shopt -u extglob
bain
  • 1,710
  • 14
  • 15
Vinko Vrsalovic
  • 330,807
  • 53
  • 334
  • 373
  • 16
    I like this feature: `ls /dir/*/!(base*)` – Erick Robertson Apr 18 '12 at 14:58
  • 7
    How do you include everything (*) and also exclude !(b*)? – Elijah Lynn Dec 23 '12 at 01:35
  • 6
    How would you match, say, everything starting with `f`, except `foo`? – Noldorin Jun 21 '13 at 20:58
  • 1
    @Noldorin,ElijahLynn See http://stackoverflow.com/questions/14822102/copy-all-so-files-excluding-libgcc-so – kennytm Apr 18 '14 at 11:27
  • 11
    Why is this disabled by default? – weberc2 Sep 29 '14 at 21:03
  • 4
    shopt -o -u histexpand if you need to look for files with exclamation points in them -- on by default, extglob is off by default so that it doesn't interfere with histexpand, in the docs it explains why this is so. match everything that starts with f except foo: f!(oo), of course 'food' would still match (you would need f!(oo*) to stop things that begin in 'foo' or, if you want to get rid of certain things ending in '.foo' use !(*.foo), or prefixed: myprefix!(*.foo) (matches myprefixBLAH but not myprefixBLAH.foo) – osirisgothra Oct 22 '14 at 11:17
  • 1
    Why is this disabled? Just to be ultra compatible, says http://stackoverflow.com/questions/17191622/why-would-i-not-leave-extglob-enabled-in-bash. Would be safe to `echo shopt -s extglob >>~/.bashrc` – Graham Perks Feb 16 '15 at 23:27
  • @osirisgothra: Why not use ‘`set +H`’ (or ‘`set +o histexpand`’) instead of ‘`shopt -o -u histexpand`’? – James Haigh May 28 '15 at 03:19
  • It's just a more verbose way of doing things, good for scripting when you are not sure of who will be doing the reading and don't wish to explain to a thousand incompetent newcomers that + does not mean disable, etc, etc... – osirisgothra Jun 12 '15 at 11:10
  • 1
    @osirisgothra extglob does not really interfere with histexpand, because if both are on, `!(...)` is ignored by history expansion. – Thus, both keep working, but `!(...)` is directed to extglob then. – Robert Siemer Jul 20 '15 at 15:19
  • 1
    One problem with shell expansion for file globbing, is that it expands the command line before it executes the program. This mean you can end up with a very long command line, and anyone else on the system can potentially see everything you've specified on the command line using `ps auxwwf` (for example). Furthermore, even if snooping users is not a concern, the command line may simply reach a limit, and the options get truncated, or the command fails to be executed. A more robust approach is always to use `find -print0 | xargs -0` as mentioned in another answer. –  Mar 26 '17 at 12:47
  • Shouldn't the first line be `-u` instead of `-s` or skipped? – dhill Mar 05 '19 at 11:35
  • is there also a way to combine positive and negative wildcards? – zeawoas May 27 '21 at 12:50
260

The extglob shell option gives you more powerful pattern matching in the command line.

You turn it on with shopt -s extglob, and turn it off with shopt -u extglob.

In your example, you would initially do:

$ shopt -s extglob
$ cp !(*Music*) /target_directory

The full available extended globbing operators are (excerpt from man bash):

If the extglob shell option is enabled using the shopt builtin, several extended pattern matching operators are recognized.A pattern-list is a list of one or more patterns separated by a |. Composite patterns may be formed using one or more of the following sub-patterns:

  • ?(pattern-list)
    Matches zero or one occurrence of the given patterns
  • *(pattern-list)
    Matches zero or more occurrences of the given patterns
  • +(pattern-list)
    Matches one or more occurrences of the given patterns
  • @(pattern-list)
    Matches one of the given patterns
  • !(pattern-list)
    Matches anything except one of the given patterns

So, for example, if you wanted to list all the files in the current directory that are not .c or .h files, you would do:

$ ls -d !(*@(.c|.h))

Of course, normal shell globing works, so the last example could also be written as:

$ ls -d !(*.[ch])
Freedo
  • 121
  • 9
tzot
  • 92,761
  • 29
  • 141
  • 204
26

Not in bash (that I know of), but:

cp `ls | grep -v Music` /target_directory

I know this is not exactly what you were looking for, but it will solve your example.

ejgottl
  • 2,809
  • 19
  • 18
12

If you want to avoid the mem cost of using the exec command, I believe you can do better with xargs. I think the following is a more efficient alternative to

find foo -type f ! -name '*Music*' -exec cp {} bar \; # new proc for each exec



find . -maxdepth 1 -name '*Music*' -prune -o -print0 | xargs -0 -i cp {} dest/
Steve
  • 581
  • 4
  • 14
8

A trick I haven't seen on here yet that doesn't use extglob, find, or grep is to treat two file lists as sets and "diff" them using comm:

comm -23 <(ls) <(ls *Music*)

comm is preferable over diff because it doesn't have extra cruft.

This returns all elements of set 1, ls, that are not also in set 2, ls *Music*. This requires both sets to be in sorted order to work properly. No problem for ls and glob expansion, but if you're using something like find, be sure to invoke sort.

comm -23 <(find . | sort) <(find . | grep -i '.jpg' | sort)

Potentially useful.

James M. Lay
  • 2,270
  • 25
  • 33
  • 1
    One of the benefits of the exclusion is not to traverse the directory in the first place. This solution does *two* traversals of sub-directories-- one with the exclusion and one without. – Mark Stosberg Dec 01 '17 at 15:42
  • 1
    Very good point, @MarkStosberg. Although, one fringe benefit of this technique is you could read exclusions from an actual file, e.g. `comm -23 <(ls) exclude_these.list` – James M. Lay Dec 05 '17 at 18:10
7

You can also use a pretty simple for loop:

for f in `find . -not -name "*Music*"`
do
    cp $f /target/dir
done
mipadi
  • 398,885
  • 90
  • 523
  • 479
  • 1
    This does a recursive find, which is different behavior than what OP wants. – Adam Rosenfield Oct 19 '08 at 21:34
  • 2
    use `-maxdepth 1` for non-recursive? – avtomaton Nov 10 '16 at 19:41
  • I found this to be the cleanest solution without having to enable / disable shell options. The -maxdepth option would be recommended in this post to have the result needed by the OP, but it all depends on what you're trying to accomplish. – davidwebca May 08 '17 at 00:40
  • 2
    Using `find` in backticks will break in unpleasant ways if it finds any nontrivial file names. – tripleee Jan 10 '18 at 11:21
  • It uses 2 loops, don't use that ever. With find use -exec like ```find . -not -name "*Music*" -exec cp "{}" /target/dir \;``` – vitalii Jul 19 '20 at 07:14
7

In bash, an alternative to shopt -s extglob is the GLOBIGNORE variable. It's not really better, but I find it easier to remember.

An example that may be what the original poster wanted:

GLOBIGNORE="*techno*"; cp *Music* /only_good_music/

When done, unset GLOBIGNORE to be able to rm *techno* in the source directory.

mivk
  • 13,452
  • 5
  • 76
  • 69
4

My personal preference is to use grep and the while command. This allows one to write powerful yet readable scripts ensuring that you end up doing exactly what you want. Plus by using an echo command you can perform a dry run before carrying out the actual operation. For example:

ls | grep -v "Music" | while read filename
do
echo $filename
done

will print out the files that you will end up copying. If the list is correct the next step is to simply replace the echo command with the copy command as follows:

ls | grep -v "Music" | while read filename
do
cp "$filename" /target_directory
done
  • 1
    This will work as long as your file names don't have any tabs, newlines, more than one space in a row, or any backslashes. While those are pathological cases, it is good to be aware of the possibility. In `bash` you can use `while IFS='' read -r filename` , but then newlines are still a problem. In general it is best not to use `ls` to enumerate files; tools like `find` are much better suited. – Thedward Mar 14 '13 at 15:16
  • Without any additional tools: `for file in *; do case ${file} in (*Music*) ;; (*) cp "${file}" /target_directory ; echo ;; esac; done` – Thedward Mar 14 '13 at 15:32
  • http://mywiki.wooledge.org/ParsingLs lists a number of additional reasons why you should avoid this. – tripleee Jan 10 '18 at 11:20
3

One solution for this can be found with find.

$ mkdir foo bar
$ touch foo/a.txt foo/Music.txt
$ find foo -type f ! -name '*Music*' -exec cp {} bar \;
$ ls bar
a.txt

Find has quite a few options, you can get pretty specific on what you include and exclude.

Edit: Adam in the comments noted that this is recursive. find options mindepth and maxdepth can be useful in controlling this.

Daniel Bungert
  • 218
  • 2
  • 6
  • This does a recursive copy, which is different behavior. It also spawns a new process for each file, which can be very inefficient for a large number of files. – Adam Rosenfield Oct 19 '08 at 21:25
  • The cost of spawning a process is approximately zero compared to all the IO that copying each file generates. So I'd say this is good enough for occasional usage. – dland Oct 19 '08 at 21:29
  • Some workarounds for the process spawning: http://stackoverflow.com/questions/186099/how-do-you-handle-the-too-many-files-problem-when-working-in-bash – Vinko Vrsalovic Oct 19 '08 at 21:34
  • use "-maxdepth 1" to avoid recursion. – ejgottl Oct 19 '08 at 21:39
  • use backticks to get the analog of the shell wild card expansion: cp `find -maxdepth 1 -not -name '*Music*'` /target_directory – ejgottl Oct 19 '08 at 21:41
  • note that find will also grab dot (hidden) files. This is different behavior than shell wildcard expansion. Obviously you can filter these out too.... – ejgottl Oct 19 '08 at 21:49
3

The following works lists all *.txt files in the current dir, except those that begin with a number.

This works in bash, dash, zsh and all other POSIX compatible shells.

for FILE in /some/dir/*.txt; do    # for each *.txt file
    case "${FILE##*/}" in          #   if file basename...
        [0-9]*) continue ;;        #   starts with digit: skip
    esac
    ## otherwise, do stuff with $FILE here
done
  1. In line one the pattern /some/dir/*.txt will cause the for loop to iterate over all files in /some/dir whose name end with .txt.

  2. In line two a case statement is used to weed out undesired files. – The ${FILE##*/} expression strips off any leading dir name component from the filename (here /some/dir/) so that patters can match against only the basename of the file. (If you're only weeding out filenames based on suffixes, you can shorten this to $FILE instead.)

  3. In line three, all files matching the case pattern [0-9]*) line will be skipped (the continue statement jumps to the next iteration of the for loop). – If you want to you can do something more interesting here, e.g. like skipping all files which do not start with a letter (a–z) using [!a-z]*, or you could use multiple patterns to skip several kinds of filenames e.g. [0-9]*|*.bak to skip files both .bak files, and files which does not start with a number.

zrajm
  • 1,361
  • 1
  • 12
  • 21
1

this would do it excluding exactly 'Music'

cp -a ^'Music' /target

this and that for excluding things like Music?* or *?Music

cp -a ^\*?'complete' /target
cp -a ^'complete'?\* /target
CoolBeans
  • 20,654
  • 10
  • 86
  • 101
gabreal
  • 1
  • 1
  • The `cp` manual page on MacOS has an `-a` option but it does something entirely different. Which platform supports this? – tripleee Jan 10 '18 at 11:19