141

I know how to delete all txt file under current directory by rm *.txt. Does anyone know how to delete all files in current directory EXCEPT txt file?

3
  • 36
    As always, test the given answers with a harmless command like ls before actually attempting to call rm. Commented Jun 5, 2013 at 15:58
  • 9
    ^ This guy doesn't like to live dangerously.. :) Commented Oct 25, 2019 at 18:19
  • 1
    ^ Guess who just did :facepalm: lol Commented Sep 11, 2022 at 1:20

9 Answers 9

212

You can use find:

find . -type f ! -name '*.txt' -delete

Or bash's extended globbing features:

shopt -s extglob
rm !(*.txt)

The entire *.txt glob should be negated to avoid matching on parts of file names (for example my.file.txt matches *.!(txt), but not !(*.txt)).

Or in zsh:

setopt extendedglob
rm *~*.txt(.)
#  ||     ^^^ Only plain files
#  ||^^^^^ files ending in ".txt"
#  | \Except
#   \Everything
9
  • 5
    Shouldn't *.!(txt) be !(*.txt)? Commented Jun 6, 2013 at 8:32
  • 1
    @LauriRanta depends on what's in the folder, which we haven't gotten an answer to. It's fine as is if all the files have extensions, and rm would choke if there were folders. Commented Jun 6, 2013 at 13:22
  • 1
    @izogfif check this find . -type f ! -name "*.txt" | xargs -r rm would work in GNU\xargs. BSD and UNIX xargs command may not have -r you have to check your local man xargs Commented Jun 8, 2018 at 7:52
  • 1
    This command does traverse directories further down, right? Commented Nov 27, 2022 at 20:56
  • 1
    @ECII the find command crawls the whole subtree, the globs do not. Commented Nov 28, 2022 at 0:21
24

If you just want to delete all files except '*.txt' then you can use the following command:

$ find . -type f ! -name "*.txt" -exec rm -rf {} \;

but if you also want to delete directories along with the files then you can use this:

$ find . ! -name "*.txt" -exec rm -r {} \;

17

there are many ways could do it. but the most simple way would be (bash):

shopt -s extglob
rm !(*.txt)
2
  • @StephenKitt is referring to an incorrect comment I made, deleted to avoid confusion: "Will also delete myfile.something.txt (I speak from bitter experience)." Commented Sep 19, 2024 at 10:46
  • @AndySwift thanks for clarifying, that helps with the review of your suggested edit! Commented Sep 19, 2024 at 11:07
10

You can use inverted grep and xargs

ls | grep -v .txt$| xargs rm
1
  • 1
    .txt$ will match strings ending with txt regardless of the dot. Because grep takes regular expression as parameter. So files a.txt and aatxt and a-txt will all be matched by this expression. Correct expression should be ls | grep -v \\.txt$ | xargs --no-run-if-empty rm. For curious people: If you want to play around with the expression safely use this test expression ls | grep \\.txt$ | xargs --no-run-if-empty echo (note: there's no -v flag and rm=>echo). Note2: you may have noticed double backslash. One is for regex, another is for bash to escape slash. Commented Nov 19, 2018 at 3:31
8

One solution without find:

mv dir/*.txt otherdir/
rm -r dir
mv otherdir dir

This should work on all kind of shells.

1
  • 1
    I think this definitely has its place as it's the answer that an occasional user is most likely to remember in the long term. Commented Mar 11, 2022 at 7:01
5

Simply do:

rm $(ls -I "*.txt" ) #Deletes file type except *.txt

Likewise, if need to delete "except one or more file type", do:

rm $(ls -I "*.txt" -I "*.pdf" ) #Deletes file types except *.txt & *.pdf

0
1

This works also to remove all hidden (dot) files and folders except the stated one (.mydir):

rm -rf $(ls -aI ".mydir")
0

I made a modular bash function for this, based on a compilation of findings:

rmexcept()
{
    files=()
    for pattern in "$@"
    do
        files+=(`find . -maxdepth 1 -type f -not -iname "$pattern"`)
    done

    # filter for duplicates only when more than one pattern provided
    if (($# > 1))
    then
        printf "%s\n" ${files[@]} | sort | uniq -d | xargs rm
    else
        printf "%s\n" ${files[@]} | xargs rm
    fi
}

It is designed to work with multiple pattern arguments:

rmexcept '*.tex' '*.pdf'

NOTE: The single quotes are necessary! Otherwise bash will expand the wildcard and you will have a number of inputs equal to the matching expansion, which causes every file to eventually repeat, and thus, causes the function to delete everything!

If you don't want to remember this dangerous caveat (I don't), define rmexcept as follows:

rmexcept()
{
    files=()
    for pattern in "$@"
    do
        files+=(`find . -maxdepth 1 -type f -not -iname "*$pattern"`)
    done

    # filter for duplicates only when more than one pattern provided
    if (($# > 1))
    then
        printf "%s\n" ${files[@]} | sort | uniq -d | xargs rm
    else
        printf "%s\n" ${files[@]} | xargs rm
    fi
}

And use without wildcards:

rmexcept .tex .pdf

NOTE: You can still make a dangerous mistake by using a prefix *. I'll keep thinking about how to improve this.


The way I put the find results in an array might not be best practice. See this thread for more details.

-1
ls |awk '!/\.txt$/{print "rm -rvf "$1}'| sh
1
  • 2
    Why not parse ls (and what to do instead)? You’ve also got a wonderful command injection there — I hope you don’t care about any of the data on any systems on which you run similar commands, or that you don’t mind your personal data (SSH keys, GPG keys for example) being shared. Commented Mar 9, 2022 at 8:31

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.