20

There are plenty of situations where the use of a * is virtually inevitable - e.g. rm -rf * in a folder that holds thousands of subfolders and files.

But what if you want to exclude just one or two files or folders from the rm command? I've googled my way around and only found quite complicated solutions like find . -depth -not \( -name 'one' -o -name 'two' \ -o -name 'three' \) -exec rm {} \; as stated here.

Is there a possibility to do this in an easier way - without that detour to find? E.g. rm -rf --exclude='one' --exclude='two' --exclude='three' * like in rsync or just rm -rf -e 'one','two','three' *?

Maybe even a general possibility to exclude things from * (so other commands like cp, mv, ... don't have to implement their own)? Something like *{'one','two','three'} or so?

3
  • 3
    If you want to keep only one or two files then the simplest and easiest way would be to move those files to another location, wipe all remaining files and move the ones you kept back. If you want to keep a lot more files I would use find with the --delete option (no need to execute rm for each file. That is needless overhead).
    – Hennes
    Aug 6, 2013 at 8:42
  • That would be a possibility, but it is nearly as elaborate as the one I mentioned. I know I could combine the commands to something like mv -t /tmp one two three && rm -rf * && mv -t . /tmp/one /tmp/two /tmp/three, but I would prefer a solution giving the possibility to explicitely exclude something from *. There will surely be situations where moving or copying the files to another destination won't be an option.
    – David
    Aug 6, 2013 at 8:57
  • 2
    It is, which is why I did not post it as an answer. Merely as a less complex way of doing things (three very simple commands vs one somewhat more complex ones). I hate complexity in combination with rm.
    – Hennes
    Aug 6, 2013 at 9:30

3 Answers 3

36

For Bash there is a shell option called extglob that is deactivated by default to keep compatibility with standard shell syntax. Extglob complements the syntax by additional operators like !(), ?(), @() and some more.

To switch extglob on, type shopt -s extglob. To keep it switched on for the current user type echo 'shopt -s extglob' >> ~/.bashrc.

For the rm example: With extglob you can use

rm -rf !(one|two|three)
7
  • Seems like a good solution, but I also don't want to switch that on and off everytime I get into a situation like the described one.
    – David
    Aug 6, 2013 at 8:51
  • 3
    @David: you could put the shopt thing into your ~/.bashrc, it cannot hurt.
    – enzotib
    Aug 6, 2013 at 8:53
  • If I get it right, the extglob option does not change the effect of * at all, but instead adds similar indicators (like the !) that have additional functions? In that case it would be a lovely solution.
    – David
    Aug 6, 2013 at 9:09
  • 1
    @David: Exactly.
    – choroba
    Aug 6, 2013 at 9:19
  • 1
    @David To keep compatibility with standard shell syntax. There are likely other situations where behaviour may change.
    – gerrit
    Aug 6, 2013 at 10:00
4

I've built except exactly for that (sry that I wasn't able to add documentation yet).

I you have a folder like the following:

$ ls
a b c d

Typing except b d will give you a and c

$ except b d
ac

Now you can pipe that to rm.

except b d | xargs -0 rm

This may be hard to remember, so why not just build a script on top of except, called rm-except:

#!/usr/bin/env bash

except "$@" | xargs -0 rm

Similarly easy is ls-except:

#!/usr/bin/env bash

except "$@" | xargs -0 ls
1
2

As an alternative to extglob (though that is a very good answer and everyone should have shopt -s extglob globstar in their .bashrc), you can use the $GLOBIGNORE global variable. Let's say you want to get every file except 'foo.txt' and 'bar baz.txt':

GLOBIGNORE=foo.txt:'bar baz.txt'

...however, this will turn the shell option dotglob on, which means that * will match files beginning with a dot (which are normally hidden). So you actually need two commands:

GLOBIGNORE=foo.txt:'bar baz.txt'
shopt -u dotglob

Since this is a global variable, it will affect every glob you use until $GLOBSTAR is unset either by logging out or with

GLOBIGNORE=

It will also only work on the literal strings passed to the variable. You can see what I mean by setting $GLOBIGNORE and looking at the difference between these commands:

printf '%s\n' *
printf '%s\n' ./*

You must log in to answer this question.

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