130

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?

3
  • 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, 2014 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. Oct 24, 2014 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, 2014 at 17:33

5 Answers 5

119
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

5
  • I tried doing the same but with brackets and apparently when using brackets it does not work
    – Donato
    May 26, 2015 at 15:40
  • 6
    What is the syntax to match multiple file names? say I want to exclude .org, .png, .txt ?
    – Freedo
    Jan 17, 2018 at 1:57
  • @Freedo @() for a pattern-list: stackoverflow.com/a/217208/3779853
    – phil294
    Aug 22, 2019 at 16:51
  • 1
    is there a way to do the same when using /bin/sh?
    – ILIV
    Nov 21, 2019 at 15:32
  • For plain sh, I'd use a for-loop with a case statement inside to decide which files to keep. Nov 21, 2019 at 17:01
46

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.

5
  • 14
    @Ruslan That doesn't work. You should do ( GLOBIGNORE="*.org"; rm -i foo* )
    – DBedrenko
    Mar 9, 2017 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 8, 2019 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* Jul 15, 2021 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 7, 2021 at 16:46
  • Worth noting that there are bugs in how this variable is processed. Let's say, we have touch a.deb b.deb c.deb. So, first of all, it's completely ignored when it's inherited. So e.g. this way GLOBIGNORE="/tmp/a.*" bash -c 'ls /tmp/*.deb still prints a.deb even though the bash subcommand sees it just fine. Then, two globs don't work either, so e.g. this command bash -c 'GLOBIGNORE="*a.*"; ls /tmp/*.deb' prints a.deb as well.
    – Hi-Angel
    Apr 23 at 13:32
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).

7
  • Useful if needed in just one line. Jan 1, 2019 at 2:31
  • 1
    Easier to remember (for me, I do that sort of thing often) and allows more complex processing to boot :)
    – drevicko
    Jul 4, 2019 at 7:17
  • 4
    ls -1 should be more reliable to ensure all files are handled by grep separately Sep 23, 2019 at 3:30
  • 3
    @MichaelChirico We have find for that, never parse ls output. Mar 9, 2020 at 10:27
  • 2
    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. May 26, 2021 at 19:58
5

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 {} +
1
  • Find with ! -name works really well!
    – phyatt
    Aug 18, 2023 at 21:22
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
1
  • This seems to be an answer to some related question about renaming files, but the current question asks about removing files.
    – Kusalananda
    May 16, 2021 at 7:41

You must log in to answer this question.

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