448

I have a directory containing a large number of files. I want to delete all files except for file.txt . How do I do this?

There are too many files to remove the unwanted ones individually and their names are too diverse to use * to remove them all except this one file.

Someone suggested using

rm !(file.txt)

But it doesn't work. It returns:

Badly placed ()'s 

My OS is Scientific Linux 6.

Any ideas?

7
  • 28
    move the one you want to keep, then rm the others? Commented Sep 5, 2014 at 9:04
  • 6
    @OlivierDulac Are we the only two people in this question who aren't overthinking the question? Commented Sep 5, 2014 at 12:33
  • 3
    @shadur: well, I can relate to them: oneliners are attractive... ^^ Commented Sep 5, 2014 at 12:57
  • 3
    Thanks, and yes I was looking for a one-line solution. It's too time consuming to keep moving files around as I have to do this quite often. Commented Sep 9, 2014 at 4:37
  • If your specifically want to preserve .git directory, see: stackoverflow.com/a/22347541/565877 Commented Jun 20, 2021 at 22:08

10 Answers 10

500

POSIXly (without -delete, -mindepth etc.):

find . ! -name 'file.txt' -type f -exec rm -f {} +

will remove all regular files (recursively, including hidden ones) except any files called file.txt.

To remove directories, change -type f to -type d and add -r option to rm; care must be taken to avoid deleting any parent directory of the directory you want to keep (add at least ! -name .).

In bash, to use rm -- !(file.txt), you must enable extglob which enables a subset of ksh's extended glob operators including the !(...) negation one:

$ shopt -s extglob 
$ rm -- !(file.txt)

(or calling bash -O extglob)

Also enable the dotglob one to also remove hidden files.

With rm -- !(file.txt), since all the matching files are passed at once to rm, that can cause an Argument list too long error.

find's -exec ... {} + works around that by running rm as many times as necessary.

In ksh93, you can do the same with:

command -x rm -- !(file.txt)

In zsh, you can use ^ to negate pattern with extendedglob enabled:

$ setopt extendedglob
$ rm -- ^file.txt

or using the same syntax with ksh and bash with options ksh_glob and no_bare_glob_qual enabled (or emulate ksh for an even closer emulation of ksh's behaviour).

To work around the too many arguments issue, you can use its zargs autoloadable function.

To remove hidden files, add the D glob qualifier (or enable dotglob like in bash, but that would affect all globs like in bash). To remove only regular files, add the . qualifier. And use **/ for recursive globbing. So an equivalent of the first find command would look like:

autoload zargs
zargs ./**/*(D.oN) -- rm -f

(adding oN to skip sorting for an even closer match).

16
  • 15
    Specifying the directory is good practice (fullpath in this case? or maybe add a warning here that this command deletes every file starting from the current working directory ?). I also usually write any example with rm as echo rm instead, and ask people to only take out the echo when they are really sure it will do what they want. Other than that, +1 for the thorough answer Commented Sep 5, 2014 at 9:00
  • 6
    How to exclude list of files? Commented Sep 6, 2014 at 4:48
  • 5
    @Meysam - see my answer for a solution that will handle a list of files. Else with Gnouc's find solution you can do ! \( -name one_file -o -name two_file \) and so on. Commented Sep 6, 2014 at 15:35
  • 3
    If you use rm -r, you'll remove the directory that contains file.txt, which will effectively remove the file that you wanted to keep. Commented Sep 10, 2014 at 19:16
  • 3
    @cuonglm ,its better if anyone explain why + at the end because i've often see ; Commented Nov 9, 2016 at 12:29
198

Another take in a different direction (iff there are no spaces, tabs, newlines, single quotes, double quotes or backslash characters¹ in file names), using -f so as to avoid the prompts that couldn't be answered as the input comes from either a pipe or /dev/null depending on the xargs implementation:

ls | grep -xvF file.txt | xargs rm -f --

or, with GNU parallel (not the parallel from moreutils), which would have problems with fewer characters² but means running one sh and one rm for each file:

ls | grep -xvF file.txt | PARALLEL_SHELL=sh parallel rm -f --

For arbitrary file names, on recent GNU systems and with a shell with support for Korn-style process substitution, leaving rm's stdin unaffected:

xargs -r0a <(ls --zero | grep -zxvF file.txt) rm --

Though we can also do without grep as GNU ls has a --ignore option to skip entries matching a pattern:

xargs -r0a <(ls --zero --ignore=file.txt) rm --

from man grep on a GNU system:

 -v, --invert-match
          Invert the sense of matching, to select non-matching lines.

 -F, --fixed-strings
          Interpret PATTERNS as fixed strings, not regular expressions.

 -x, --line-regexp
          Select  only  those  matches  that exactly match the
          whole line.  For a regular expression pattern,  this
          is   like   parenthesizing   the  pattern  and  then
          surrounding it with ^ and $.

 -z, --null-data
          Treat input and output data as sequences of lines, each
          terminated by a zero byte (the ASCII NUL character) instead
          of a newline.  Like the -Z or --null option, this option can
          be used with commands like sort -z to process arbitrary file
          names.

(-x, -v, -F are standard, the rest including the long forms of the standard options are GNU extensions).

Without the -x we'd keep my-file.txt as well and without -F, we'd also keep file-txt.


¹ and with some xargs implementations, possibly other blank characters as classified as such in the locale, and no file called _ and no file that can't be decoded as text in the locale.

² Normally only newlines, but older versions of GNU parallel did not always properly quote all strings before passing to the shell so YMMV; we mitigate it here by forcing the shell to be sh with which the quoting is easier and syntax well known.

13
  • 7
    It will not work if file names have a space ... Commented Feb 8, 2016 at 16:07
  • 4
    @Sebastian The problem is not the grep but rm. rm will get a list of space separated arguments. Try touch 'a b'; touch 'c d'; ls | grep -v 'a b' | xargs rm: you will get rm: c: No such file or directory and rm: d: No such file or directory Commented Feb 9, 2016 at 7:57
  • 4
    It's not only spaces, it's all blanks and newlines, but also quoting characters (single, double quotes and backslash) and filenames starting with -. Commented Aug 30, 2016 at 9:52
  • 23
    This worked for me ls -Q | grep -v file.txt | xargs rm -fr . -Q switch is "enclose entry names in double quotes" Commented Aug 24, 2019 at 13:11
  • 3
    The dot character into the grep search pattern is matching any character if it is not escaped like 'file\.txt' to match literal dot only. For example, it will also match a file named filestxt. Commented Jun 7, 2022 at 21:22
51
+50

Maintain a copy, delete everything, restore copy:

{   rm -rf *
    tar -x
} <<TAR
$(tar -c $one_file)
TAR

In one line:

{ rm -rf *; tar -x; } <<< $(tar -c $one_file)

But that requires a shell that supports here-strings.

13
  • 27
    This is somewhat mind-blowing. Commented Sep 5, 2014 at 2:36
  • 3
    But if this gets interrupted halfway through, or if anything else goes wrong, the file is gone. Commented Sep 5, 2014 at 8:04
  • 3
    Isn't more efficient to move it to another directory and move it back? We don't need to deal with the content of the file, only with its path. Commented Sep 6, 2014 at 15:51
  • 5
    @Derek - it's really not that crazy. POSIX requires that a shell redirect its input to the command you specify when it encounters a here-document. The command-substitution has to complete before anything else happens. Most shells use temp-files for here-docs - some pipes. Either way tar -c completes and the shell stashes its output before rm runs. Because rm ignores stdin its left hanging for tar -x when rm finishes - and the shell can divest itself of the copy it saved of your file(s). Here-docs can be used like aimed pipes a lot of the time. Commented Sep 9, 2014 at 5:27
  • 6
    What if the file is 64GB in size? Or is there no actual copying involved? Commented Jul 13, 2018 at 19:04
42

you're all overthinking this.

cd ..
mv fulldir/file.txt /tmp/
rm -rf fulldir
mkdir fulldir
mv /tmp/file.txt fulldir/

Done.

EDIT Actually, easier:

cd ..
ln fulldir/file.txt ./
rm -rf fulldir
mkdir -p fulldir
mv file.txt fulldir/
7
  • 7
    that's the same thing my answer does. exce[pt it doesn't lose any permissions on the dir. Commented Sep 5, 2014 at 12:50
  • 9
    If it's a large file on a separate filesystem from /tmp and you're trying to remove everything from the root of the filesystem down, then moving it somewhere safe may not be an option. Commented Sep 6, 2014 at 5:24
  • 4
    This is definitely the simplest answer. Commented Oct 18, 2016 at 23:10
  • 1
    Both fail if fulldir is a mountpoint. Both result in incorrect settings if fulldir isn't "your" directory with standard permissions Commented Jul 4, 2020 at 10:50
  • 1
    Also, I wonder if someone out there knows how to preserve directory permissions.. feels like there must be a way.. Either way, mving files outside directory, and then obliterating contents in some way, seems like the best, safest, most obvious approach. Maybe everyone on this StackExchange is really good with shell scripts. Commented Jun 20, 2021 at 22:08
26

On my Scientific Linux 6 OS this works:

shopt -s extglob
rm !(file.txt)

I also have Debian 32bit installed on a Virtual Machine. The above does not work but the following does:

find . -type f ! -name 'file.txt' -delete
7
  • 1
    You mean the find solution I gave didn't working? Commented Sep 5, 2014 at 2:30
  • "didn't work" or " isn't working". Yes your find solution also works. Thanks. Commented Sep 5, 2014 at 2:43
  • 1
    Can you make it more details? How "didn't work? It does not remove other files, or it removed file.txt or anything else? Commented Sep 5, 2014 at 2:45
  • I meant that on the Debian OS "$rm !(file.txt)" returns "Badly placed ()'s". So I tried "$shopt -s extglob", but it returned: "shopt: Command not found". Then I tried the "find" solution. It does work. Commented Sep 5, 2014 at 2:55
  • 1
    Oh, so of course it doesn't work. shopt is not a tcsh builtin. Commented Sep 5, 2014 at 3:15
14

I find that this approach is very simple, works, and doesn't require any special extensions (that I know of!)

ls --hide=file.txt | xargs -d '\n' rm
1
  • 1
    The simple solution for files without new lines, quotes and such Commented Jun 6, 2022 at 21:27
12

Use rm !("file.txt") instead of rm !(file.txt)

2
  • 14
    That makes absolutely no difference whatsoever. The issue here was that the OP was 1) not using a shell that supports this format and 2) even in bash, you need to enable it with shopt -s extglob. In any case, quoting a simple filename like that would have made no difference. Commented Sep 6, 2014 at 11:15
  • 1
    To keep just utils folder do - rm -rf !("utils") Commented Feb 1, 2018 at 18:27
8

Just to give a different answer, you can use the default behavior of rm that it won't delete folders:

mkdir tmp && mv file.txt tmp  # create tmp dir and move files there
rm                            # delete all other files
mv tmp/* . && rm -rf tmp      # move all files back and delete tmp dir
1
  • 1
    What would this do if ./tmp already exists as a file or, worse, as a directory with things already in it? Using dirr=mktemp && mv file.txt "$dirr"; rm; mv "$dirr/*" . && rm -rf "$dirr" would avoid this issue. Commented Jan 26, 2020 at 13:52
0

In my case I needed to remove all files and folder except for zip files, inspired on accepted answer (I gave of course a +1), while from folder I want to clean all except zip files I use find . ! -name '*.zip' ! -name '.' ! -name '..' -exec rm -rf {} +, for example (all files/folders are empty since created just for the example):

my-computer:/tmp/toclean>ll
total 0
-rw-r--r-- 1 user users 0 Oct  4 10:47 aaa.zip
-rw-r--r-- 1 user users 0 Oct  4 10:47 tata
drwxr-xr-x 1 user users 0 Oct  4 10:47 titi
drwxr-xr-x 1 user users 0 Oct  4 10:47 tutu
-rw-r--r-- 1 user users 0 Oct  4 10:47 yoyo
-rw-r--r-- 1 user users 0 Oct  4 10:47 zzz.zip
my-computer:/tmp/toclean>find . ! -name '*.zip' ! -name '.' ! -name '..'  -exec rm -rf {} +
my-computer:/tmp/toclean>ll
total 0
-rw-r--r-- 1 user users 0 Oct  4 10:47 aaa.zip
-rw-r--r-- 1 user users 0 Oct  4 10:47 zzz.zip

Note I exclude '.' and '..' to just to avoid rm: refusing to remove '.' or '..' directory: skipping '.' message.

0

EZ.

mkdir -p empty_dir && rsync -av --exclude 'file.txt' --exclude 'folder/' --delete empty_dir/ ./

An empty directory is created where rsync transfers all files and directories except for the excluded ones, and deletes everything that was copied from the original location.

Make sure to put the trailing slash for folders as it matters to rsync.

1
  • Hmm ... what exactly is the folder/ you are excluding? Commented Oct 30, 2024 at 13:46

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.