Take the 2-minute tour ×
Super User is a question and answer site for computer enthusiasts and power users. It's 100% free, no registration required.

Recently I was faced with the task of deleting all the files/folders in a directory excluding those matching a specific pattern. So I cooked up a single-line unix command to do the work. Must it be only one line? I suppose not, but it's definitely cooler that way!

While the problem is pretty simple, I was a little surprised at how complex my solution ended up being. Here's the command I used; NOTE: this is a poor solution because it doesn't handle filenames containing line-feed characters (which didn't matter in my situation).

ls | grep -v PATTERN | xargs -n1 -IREPLACE rm -rf REPLACE

I did not use the "find" command because I do not want to recurse into folders matching PATTERN. For example, consider the following file structure:

file_foo.txt
first_dir
  |
  +--> contents
  +--> ...
foo_dir
  |
  +--> anotherfile.txt
  +--> morefiles.log
foo_file.txt
somefile.txt

Using pattern "foo" must only remove "first_dir" (and it's contents of course) and "somefile.txt" (not "anotherfile.txt" or "morefiles.log").

Question, are there better (more elegant and correct) ways to accomplish this?


EDIT:
It was recently brought to my attention that "find" may be a better option:

find * -maxdepth 0 ! -name PATTERN -print0 | xargs -0n1 rm -rf

This solution does correctly handle paths containing line-feed characters.

share|improve this question
    
EDIT: changed erroneous "the_dir" to correct "first_dir". –  joxl Dec 15 '10 at 4:13

3 Answers 3

up vote 8 down vote accepted

The following examples have echo prefixed so that you can test the patterns before actually using them. Remove the echo to activate the rm -rf. Substitute rm -ri to prompt for confirmation.

ksh has a negative match extension to its globbing:

# ksh
echo rm -rf !(*foo*)

The same syntax is available in bash if you set the extglob option:

# bash
shopt -s extglob
echo rm -rf !(*foo*)

zsh has its own syntax for this:

# zsh
setopt extended_glob
echo rm -rf ^*foo*

It can also use the ksh-style syntax:

# zsh: ksh-style glob syntax
setopt ksh_glob no_bare_glob_qual
echo rm -rf !(*foo*)

# zsh: ksh-style glob syntax, adapted for the default bare_glob_qual option
setopt ksh_glob bare_glob_qual
echo rm -rf (!(*foo*))
share|improve this answer
    
Simple, elegant, handles linefeed characters, (almost) one line (I've added shopt -s extglob to my .bashrc file, so it now is a one-liner). Perfect! I promise an upvote as soon as my reputation allows it. –  joxl Dec 15 '10 at 4:04

Here's another find solution. I'm not sure this has any real advantage over yours, but it doesn't need xargs and allows for the rare possibility that * expands to too many names.

find . -maxdepth 1 ! -name PATTERN -type f -delete

I also added -type f so that it would not attempt to delete directories.

Warning: -delete is powerful. I gave one of my test files 0 permissions and the command above deleted it without hesitation.

share|improve this answer
5  
Test first. Replace -delete with -print to see if it's finding the files you wanted. If all is good, use the command history (e.g. press the up button) to get back to the previous command to ensure that you're working with the same find filters. –  Doug Harris Jul 27 '10 at 14:47
    
Read carefully, note that I do wish to remove directories. And find -delete will not remove non-empty directories. Also notice that find -maxdepth 1 matches "." (current directory) which is really bad. Although I do like your idea of eliminating xargs, one might use find -exec alternatively. find * -maxdepth 0 ! -name PATTERN -exec rm -rf '{}' \; –  joxl Jul 27 '10 at 22:36

You could use this rebol script example http://askcodegeneration.com/keep-subversion/ instead which is much more understandable and which can manage user interaction like folder selection and confirmation:

delete-file: func[file][
    if/else none? find file "/.svn/" [
        delete file
    ][
        print ["keeping" file]
    ]
]
dir: request-dir
ans: ask rejoin ["Do you confirm " dir "? (Yes): "]
if (ans = "Yes") [
    foreach-file dir :delete-file
]
share|improve this answer

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

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