128

I have several files with the same base filename. I'd like to remove all but one

foo.org #keep
foo.tex #delete
foo.fls #delete
foo.bib #delete
etc

If I didn't need to keep one, I know I could use rm foo.*.

TLDP demonstrates ^ to negate a match. Through trial and error, I was able to find that

rm foo.*[^org]

does what I need, but I don't really understand the syntax.

Also, while not a limitation in my use case, I think this pattern also ignores foo.o and foo.or. How does this pattern work, and what would a glob that ignores only foo.org look like?

jake
  • 1,487
  • 6
    As an addition to @glen answer it is worth to mention that rm foo.*[^org] removes all files which last character is neither o, r or g, so foo.foo wouldn't match either. – jimmij Oct 24 '14 at 16:04
  • 1
    You are using a Regular Expression. You should be careful with your grouping characters. By using brackets you've specified a character class meaning that you would delete any files that had an extension with the letters o,r or g in any order. Use parenthesis to create a group and preserve the order of characters. – Mr. Mascaro Oct 24 '14 at 17:16
  • 6
    @jbarker2160 - that's not really a regular expression, it's more commonly called a glob (or filename pattern), which is more or less a subset of a regular expression - see the pattern matching section of the bash manpage for details. His pattern foo.*[^org] will match any filename that begins with foo. with one or more characters after the dot where the last character is not o, r, or g. So it would match foo.orb, but not foo.org or foo.or or foo.o. GlennJackman's answer shows how to enable extended pattern matching features to negate a match. – Johnny Oct 24 '14 at 17:33

5 Answers5

118
shopt -s extglob
echo rm foo.!(org)

This matches foo. followed by anything NOT org

To restore normal glob behavior afterwards, use:

shopt -u extglob

Reference: The GNU Bash reference manual

AdminBee
  • 22,803
glenn jackman
  • 85,964
43

In bash, you can also use GLOBIGNORE="*.org"; rm -i foo*.

And unset GLOBIGNORE when done.

It's not really better than shopt -s extglob, but I find it easier to remember.

mivk
  • 3,596
  • 14
    @Ruslan That doesn't work. You should do ( GLOBIGNORE="*.org"; rm -i foo* ) – DBedrenko Mar 09 '17 at 11:20
  • 16
    Note if you want to expand the list of ignored, separate with a colon, such as GLOBIGNORE="*foo*:*bar*" – phyatt May 08 '19 at 18:45
  • 2
    Hmmm I ended up needing to add a && between setting the GLOBIGNORE and the command following where it is supposed to be applied. I assume that if you don't, the entire expression is evaluated and all * snippets are expanded - at which point it is too late to set GLOBIGNORE. In other words, for this example: GLOBIGNORE="*.org" && rm -i foo* – consideRatio Jul 15 '21 at 18:56
  • 2
    Thank you @consideRatio for the explanation. So that's why the subshell and semicolon were suggested by DBedrenko. The OP simply doesn't work and any version that does work leaves you an environment variable to clean up. – Ron Burk Aug 07 '21 at 16:46
14

A pipe can do?

ls * | grep -v "foo.org" | xargs -I {} echo {}

(obviously you might want to replace echo with rm in the last chain).

Adobe
  • 445
  • Useful if needed in just one line. – bruceskyaus Jan 01 '19 at 02:31
  • 1
    Easier to remember (for me, I do that sort of thing often) and allows more complex processing to boot :) – drevicko Jul 04 '19 at 07:17
  • 4
    ls -1 should be more reliable to ensure all files are handled by grep separately – MichaelChirico Sep 23 '19 at 03:30
  • Unfortunately, the side-effect of xargs in this form is that each process (and therefore file) waits to complete before the next process runs. So, in the case of something like xz -T0 multithreaded compression will be limited to within the file which reduces the efficiency considerably on multicore systems. You can add the -P flag to xargs which helps, but still means the compression indexes have to be rebuilt separately for every file. – tudor -Reinstate Monica- Jan 12 '20 at 23:45
  • 3
    @MichaelChirico We have find for that, never parse ls output. – val - disappointed in SE Mar 09 '20 at 10:27
  • Note that ls * would list the contents of any directory matching *. It would also delete any file whose name does not match foo.*. – Kusalananda May 24 '21 at 21:26
  • 1
    In almost every case echo * is preferable to ls *, when scripting. One problem with echo * is that it doesn't hit hidden files. echo * .* does that. – Tripp Kinetics May 26 '21 at 19:58
4

The pattern foo.*[^org], which is properly written foo.*[!org] (but bash can use ^ in place of !) would match any name that starts with foo. and ends in something that is not o, r or g. The [...] bit always matches exactly one single character. This would not match foo.org as it ends with g, but it would also not match foo.log for the same reason.

A portable answer would be to use a loop, and then to avoid acting on the file that we want to keep:

for name in foo.*; do [ "$name" != foo.org ] && rm -- "$name"; done

If you want to avoid deleting any file matching foo.* and ending in .org (e.g. foo.org or foo.beef.org etc.), then consider using a second pattern match like so:

for name in foo.*; do case $name in (*.org) continue;; esac; rm -- "$name"; done

Using find instead, the first loop above would be equivalent to

find . ! -path . -prune -name 'foo.*' ! -name foo.org -exec rm {} +

while the second one would be the same as

find . ! -path . -prune -name 'foo.*' ! -name '*.org' -exec rm {} +
Kusalananda
  • 333,661
0

Another way to accomplish. Suppose that you have some file foo.sh that you want exclude from your operation, yet you want to add csv to every other file. You could use the following loop:

for f in *; do
  if [ "${f: -3}" != ".sh" ]; then
    mv "$f" "${f%}.csv"
  fi
done
  • This seems to be an answer to some related question about renaming files, but the current question asks about removing files. – Kusalananda May 16 '21 at 07:41