52

I have a directory that contains the following:

x.pdf
y.zip
z.mp3
a.pdf

I want to delete all files apart from x.pdf and a.pdf. How do I do this from the terminal? There are no subdirectories so no need for any recursion.

13 Answers 13

67
cd <the directory you want>
find . -type f ! -iname "*.pdf" -delete
  • The first command will take you to the directory in which you want to delete your files
  • The second command will delete all files except with those ending with .pdf in filename

For example, if there is a directory called temp in your home folder:

cd ~/temp

then delete files:

find . -type f ! -iname "*.pdf" -delete

This will delete all files except xyz.pdf.

You can combine these two commands to:

find ~/temp -type f ! -iname "*.pdf" -delete

. is the current directory. ! means to take all files except the ones with .pdf at the end. -type f selects only files, not directories. -delete means to delete it.

NOTE: this command will delete all files (except pdf files but including hidden files) in current directory as well as in all sub-directories. ! must come before -name. simply -name will include only .pdf, while -iname will include both .pdf and .PDF

To delete only in current directory and not in sub-directories add -maxdepth 1:

find . -maxdepth 1 -type f ! -iname "*.pdf" -delete
7
  • Thanks for the answer. Can you help me understand the syntax a little? . means "and"? ! means "except" -name signifies that you want to exclude by a name parameter and then -delete is the action to take upon finding? So it looks for everything except "*.pdf" and deletes them? Or have I misunderstood? Dec 1, 2014 at 12:50
  • . means current directory. ! means to take all files except the one with .pdf at the end. -delete means to delete it. am i clear now ?
    – Alex Jones
    Dec 1, 2014 at 12:57
  • @terdon Starkers said that there are no sub-directories.wait ill edit my answer to be more broad
    – Alex Jones
    Dec 1, 2014 at 13:23
  • +1 You should have included the -maxdepth 1 parameter to begin with. Then suggest removing the parameter in case one wants to delete recursively. Dec 2, 2014 at 12:59
  • 3
    this brought to my attention that we should be using -iname instead of -name, or files with .PDF as an extension will slip through.
    – muru
    Dec 3, 2014 at 17:01
45
+100

With bash's extended shell globbing, you could remove any files with extensions other than .pdf using

rm -- *.!(pdf)

As noted by @pts, the -- characters indicate the end of any command options, make the command safe in the rare case of files whose names start with a - character.

If you want to delete files without any extension as well as those with extensions other than .pdf, then as pointed out by @DennisWilliamson you could use

rm -- !(*.pdf)

Extended globbing should be enabled by default, but if not you can do so using

shopt -s extglob

Especially if you intend to use this inside a script, it's important to note that if the expression doesn't match anything (i.e. if there are no non-pdf files in the directory), then by default the glob will be passed unexpanded to the rm command, resulting in an error like

rm: cannot remove `*.!(pdf)': No such file or directory

You can modify this default behaviour using the nullglob shell option, however that has its own issue. For a more thorough discussion see NullGlob - Greg's Wiki

7
  • Better approach IMO.
    – Takkat
    Dec 1, 2014 at 12:21
  • 1
    What about files without an extension? FWIW, in zsh it’s rm *~*.pdf Dec 1, 2014 at 15:18
  • 1
    I would put the dot inside the parentheses. Dec 1, 2014 at 20:39
  • 4
    Ah, the asterisk should also go inside: !(*.py). Also, presumably, if the OP wants only ".pdf" files remaining, then files without extensions should also be deleted and not ignored. Dec 1, 2014 at 20:50
  • 1
    This approach is simpler and neater than the accepted answer.
    – Peter
    Dec 3, 2014 at 10:31
19

Delete to trash:

$ cd <the directory you want>
$ gvfs-trash !(*.pdf)

Or via mv command (but in this way you cannot restore it from Trash since it doesn't record .trashinfo information, so this means you moved your files to a destination where it is as following).

mv !(*.pdf) ~/.local/share/Trash/files
1
  • 7
    This approach is much safer than directly using rm.
    – Seth
    Dec 2, 2014 at 20:13
14

The easiest approach: Create another directory somewhere (if you're only deleting in one directory, not recursively, it can even be a subdirectory); move all the .pdf's there; delete everything else; move the pdf's back; delete the intermediate directory.

Quick, easy, you can see exactly what you're doing. Just make sure the intermediate directory is on the same device as the directory you're cleaning up so that moves are renames, not copies!

1
  • 4
    +1 Again for a comment that makes sense to the novice user, that will almost certainly not result in deleting files unintentionally. Dec 2, 2014 at 18:59
4

Use bash's GLOBIGNORE:

GLOBIGNORE=x.pdf:a.pdf
rm *
unset GLOBIGNORE

From bash's man page:

GLOBIGNORE:

            A colon-separated list of patterns defining the set
            of filenames to be ignored by pathname expansion.

A quick test:

mkdir /tmp/foooooo
cd /tmp/foooooo
touch x.pdf y.zip z.mp3 a.pdf
GLOBIGNORE=x.pdf:a.pdf
ls -1 *

Output:

y.zip
z.mp3
3

Be careful and compose: use xargs

Here's an approach I like, because it lets me be very careful: compose a way to show just the files I want to delete, then send them to rm using xargs. For example:

  • ls shows me everything
  • ls | grep pdf shows me the files I want to keep. Hmm.
  • ls | grep -v pdf shows the opposite: all except what I want to keep. In other words, it shows the list of things I want to delete. I can confirm this before doing anything dangerous.
  • ls | grep -v pdf | xargs rm sends exactly that list to rm for deletion

As I said, I mainly like this for the safety it provides: no accidental rm * for me. Two other advantages:

  • It's composable; you can use ls or find to get the initial list, as you prefer. You can use anything else you like in the process of narrowing that list - another grep, some awk, or whatever. If you needed to delete only files whose names contain a color, you could build it up this same way.
  • You can use each tool for its main purpose. I prefer to use find for finding and rm for removal, as opposed to having to remember that find accepts a -delete flag. And if you do this, again, you can compose alternate solutions; maybe instead of rm, you could create a trash command that moves the file to the trash (allowing "undeletion") and pipe to that instead of rm. You don't need to have find support that option, you just pipe to it.

Update

See comments by @pabouk for how modify this to handle some edge cases, such as line breaks in file names, filenames like my_pdfs.zip, etc.

4
  • 4
    I noticed three problems here: a) It will exclude any file containing pdf anywhere in its name. --- b) It will delete PDF files if any of the letter in the suffix are upper case. --- c) It is not a good idea to use output of ls. It will not work with file names containing newlines. Some implementations of ls replace special characters e.g. tab by ?. --- It is better to use: find -maxdepth 1 -print0. (not so short as ls :) ----- To resolve a) and b) use grep -vi '\.pdf$' --- complete (but GNU only) solution: find -maxdepth 1 -print0 | grep -viz '\.pdf$' | xargs -0 rm Dec 1, 2014 at 22:12
  • 1
    I understand that you meant the solution as an "interactive" process with multiple manual iterations but the checks will be hardly usable for long lists of files and the problems mentioned above could bring easy to overlook mistakes. Dec 1, 2014 at 22:22
  • 1
    @pabouk good points; the real world always complicates things, and your corrections are helpful. :) But I still think this overall approach is best. If there are too many files to visually confirm everything, you can | head -20 to at least see if it looks roughly correct, whereas if you just rm my_pattern, you have no chance to spot a big mistake. Dec 2, 2014 at 14:13
  • 1
    You can have find show you the files before you delete them too, leave out the -delete and just use find . -type f ! -name "*.pdf" to print to console, or pipe to less or a file. [and then pipe to xargs to rm if desired like pabouk's comments (with the -print0| ... -0 for weird filenames)]
    – Xen2050
    Dec 3, 2014 at 7:26
3

I usually solve such problems from the interactive Python interpreter:

mic@mic ~ $ python
>>> import os
>>> for f in os.listdir('.'):
...   if not f.endswith('.pdf'):
...     os.remove(f)

It might be longer than a one-liner with find or xargs, but it's extremely resilient, and I know exactly what it does, without having to research it first.

9
  • For those who get increasingly nervous with every additional line, we could make it into one: for item in [f for f in os.listdir('.') if not f.endswith('.pdf')]: os.remove(item) Dec 4, 2014 at 16:59
  • python -c "import os; for f in os.listdir('.'): if not f.endswith('.pdf'): os.remove(f)"
    – mic_e
    Dec 4, 2014 at 17:36
  • [os.remove(f) for f in os.listdir('.') if not f.endswith('.pdf')]
    – mic_e
    Dec 4, 2014 at 17:37
  • nice! the second one gives me a syntax error, don't see why. Dec 4, 2014 at 17:48
  • strange; it works with both python 3.4 and python 2.7 on my system.
    – mic_e
    Dec 4, 2014 at 17:56
2

Better answer (compared to my previous answer) to this question would be by using the powerful file command.

$ file -i abc.pdf
abc: application/pdf; charset=binary

now your problem:

cd <the directory you want to search in>
for var in ./*
do
  if file -i "$var" | grep -q 'application/pdf\;'
    then
    echo "$var"
  fi
done

The job of for command is give the files in current directory in the form of variable $var. if-then command outputs the names of pdf files by taking the exit status of 0 from file -i "$var" | grep -q 'application/pdf\;' command, it will give exit status of 0 only if it finds pdf files.

1
rm $(ls -lo|grep -v [Pp][Dd][Ff]$|awk '{print $7}')

Warning! Better try first

ls -l $(ls -lo|grep -v [Pp][Dd][Ff]$|awk '{print $7}')
2
1
rm -i -- !(*@(a|x).pdf)

Read as, remove all files that are not a.pdf or x.pdf.

This works by making use of 2 extended globs, the outer !() to negate the contained glob which itself requires that the glob must match one or more of a or x patterns before the .pdf suffix. See glob#extglob.

$ ls -a
.dotfile1 .dotfile2 a.pdf x.pdf y.zip z.mp3

$ echo -- !(a.pdf)
-- x.pdf y.zip z.mp3

$ echo -- !(x.pdf)
-- a.pdf y.zip z.mp3

$ echo -- !(a.pdf|x.pdf)
-- y.zip z.mp3

$ echo -- !(@(a|x).pdf)   # NOTE.that this matches the .dotfiles* as well
-- . .. .dotfile1 .dotfile2 y.zip z.mp3

$ echo -- !(*@(a|x).pdf)  # but this doesn't
-- y.zip z.mp3

$ echo rm -i -- !(*@(a|x).pdf)
rm -i -- y.zip z.mp3
1

portable shell way

$ ksh -c 'for i in ./*; do case $i in *.pdf)continue;; *)rm "$i";; esac;done'

Pretty much POSIX and compatible with any Bourne-style shell ( ksh, bash, dash ). Well suited for portable scripts and when you can't use bash's extended shell globbing.

perl:

$ perl -le 'opendir(my $d,"."); foreach my $f (grep(-f && !/.pdf/ , readdir($d))){unlink $f};closedir $d'                                                             

Or slightly cleaner:

$ perl -le 'opendir(my $d,"."); map{ unlink $_ } grep(-f "./$_" && !/.pdf/ , readdir($d));closedir $d'

alternative python

python -c 'import os;map(lambda x: os.remove(x), filter(lambda x: not x.endswith(".pdf"),os.listdir(".")))'
0

Be Careful of what you're deleting!

A safe way to test it before trying to delete is to test first with ls, as some uncaught behaviors could delete unwanted files. And you can do it directly outside of the directory. ls is similar to rm, so :

ls sub/path/to/files/!(*.pdf)

This will list

y.zip
z.mp3

And now you can see what you're deleting and can safely delete them :

rm sub/path/to/files/!(*.pdf)

And that's it. Yo can use wildcard * to be more selective like keeping only programming course documents :

rm sub/path/to/files/!(*programming*)
0

This solution would work for most cases.

I got hundreds of directories but I have exception for some directories files.

  1. Zip all files that is not for deletion to my_files.zip. If there are directories, you need to add the recursive command.

    zip -r my_files.zip a.php b.txt c.dll directory1 directory2
    
  2. Delete all files recursively(thoroughly) except myfiles.zip

    #extended globbing allows for more advanced pattern matching.
    shopt -s extglob
    
    #delete all file
    rm -r -- !(my_files.zip)
    
  3. Unzip my_files.zip

    unzip my_files.zip
    

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .