tags:

views:

3446

answers:

6

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?

+2  A: 

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
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
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
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
use "-maxdepth 1" to avoid recursion.
ejgottl
use backticks to get the analog of the shell wild card expansion:cp `find -maxdepth 1 -not -name '*Music*'` /target_directory
ejgottl
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
+3  A: 

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
Default ls will put multiple files per line, which probably isn't going to give the right results.
Daniel Bungert
Only when stdout is a terminal. When used in a pipeline, ls prints one filename per line.
Adam Rosenfield
ls only puts multiple files per line if outputting to a terminal. Try it yourself - "ls | less" will never have multiple files per line.
SpoonMeiser
Huh, neat trick, didn't know that :)
Daniel Bungert
You could use ls -1 anyway
ypnos
It won't work for filenames containing spaces (or other white spcace characters).
ΤΖΩΤΖΙΟΥ
A: 

You can also use a pretty simple for loop:

for f in `find . -not -name "*Music*"`
do
    cp $f /target/dir
done
mipadi
This does a recursive find, which is different behavior than what OP wants.
Adam Rosenfield
+13  A: 

In Bash you can do it by enabling the extglob option, like this (replace ls for 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
Vinko Vrsalovic
+1  A: 

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
+6  A: 

The extglob shell option gives you more powerful regular expressions 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 _ext_ended _glob_bing operators are (excerpt from man bash):

If the extglob shell option is enabled using the shopt builtin, several extended pattern matching operators are recognized. In the following description, a pat‐ tern-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])
ΤΖΩΤΖΙΟΥ
This is really good! I will edit the question a little to make it more general and hopefully your answer can get voted up some more.