10

I have the following list of files:

main.acn
main.acr
main.alg
main.aux
main.glg
main.glo
main.gls
main.ist
main.lof
main.log
main.lot
main.nlo
main.out
main.pdf
main.tex
main.toc

I want to rm all of the main.* files except for main.tex and main.pdf file.

I tried rm main.* !("main.tex","main.pdf") but that did not work and removed all the main.* without exception.

Any ideas?

9
  • 2
    Which shell? If it is bash do you have extglob set? The point about wildcards is that the shell expands them, so the rm main.* will give a list of all the files to rm to remove. You need to get your shell to give the correct list to rm.
    – icarus
    Oct 28, 2022 at 19:45
  • 3
    I guess I'm lazy because I would just pipe find into grep and then xargs rm. Oct 28, 2022 at 19:55
  • 1
    @AaronD.Marasco note that that approach, which is actually considerably more typing, so I don't really see how you can call it "lazy", will fail on non-standard file names. Try it with a filename containing spaces or, even worse, a file name with a newline (touch 'file name' file$'\n'newline).
    – terdon
    Oct 30, 2022 at 19:47
  • 2
    @terdon Laziness isn't always about typing less, sometimes it's about thinking less. If it takes you 10 minutes to write a 10-line shell script, and 30 minutes to learn a neat trick to do it in with 5 characters in sed, the shell script is the lazy option. Which is better depends on what you're trying to achieve: a successful result right now, or a learning opportunity for the future.
    – IMSoP
    Oct 31, 2022 at 15:04
  • 1
    @terdon By correcting all the possible pitfalls, you've ended up with a straw man: the lazy approach referenced is likely just find . | grep some-pattern | xargs rm You were absolutely right that there are a bunch of times when that would go wrong; there are also a bunch of times when it would do exactly what's wanted, without needing to remember any option flags at all. And that's exactly what makes it lazy.
    – IMSoP
    Oct 31, 2022 at 20:54

4 Answers 4

21

!("main.tex","main.pdf") matches anything that isn't main.tex,main.pdf, so probably all files in the directory.

The key to note is that !(...) already acts like *, in that it itself matches anything that isn't one of the things inside. And the separator is |, not ,.

So, main.!(tex|pdf) should work. Or !(main.tex|main.pdf) if you want to remove every file but those two, not just the ones with names that start with main..

See:

5
  • 1
    I presume this is bash specific, this is not universal. If so, can we get the OP tagged bash? Oct 28, 2022 at 19:46
  • rm main.!(pdf|tex) did the job perfectly. Thanks!
    – Sudoh
    Oct 28, 2022 at 19:51
  • @JamesRisner This was for bash. I have tagged it for bash. Thank you for pointing it out.
    – Sudoh
    Oct 28, 2022 at 19:52
  • 1
    @JamesRisner, Bash (with shopt -s extglob), or ksh (should work by default), or zsh (with setopt kshglob). But yes, not standard POSIX shell.
    – ilkkachu
    Oct 29, 2022 at 9:37
  • @JamesRisner, that !(x) operator is from ksh from the early 80s. bash copied a subset of ksh extended operators and only enables them with the extglob option. Oct 30, 2022 at 18:10
9

The less sophisticated route is to save the files you want to keep, delete everything (else), and put the saved files back again. It's also a POSIX compliant solution:

mkdir x                   # File or directory must not already exist
mv main.tex main.pdf x    # Save the files you want to keep
rm ./*                    # Ignore the error about not deleting 'x'
mv x/* .                  # Restore everything you saved. Notice the dot!
rmdir x                   # Tidy up

Here I've picked x as a file or directory that doesn't already exist. You will get an error from rm that it cannot delete x; that's good. Do not use rm -rf!

Notice that normally rm ./* will skip dot files.

4
  • As a one-off solution, this is absolutely the least sophisticated route to go and if I hadn't been given the perfect solution by @ikkachu, this is what I would have done. But the use of rm main.!(pdf|tex) is only mildly more sophisticated, while using 4 less commands, vastly more idiot-proof, scalable and teaches me something new.
    – Sudoh
    Oct 29, 2022 at 0:47
  • 1
    The problem with rm main.!(pdf|tex) is that it depends so much on which shell you're using and which options are set, so I wouldn't want this to become idiomatic. Oct 29, 2022 at 12:35
  • 1
    You could name the directory .x instead of x, then rm ./* won't match .x in the first place, assuming nothing like dotglob is set.
    – chepner
    Oct 30, 2022 at 14:51
  • @chepner indeed, but for the purposes of the illustration I felt that using x involved less "magic" for what was intended as a totally unsophisticated solution Oct 30, 2022 at 17:27
7

In Bash there is also the GLOBIGNORE variable, which can exclude/remove names from the list returned by a glob. An example:

$ touch main.pdf main.tex one.pdf one.tex three.wow two.pdf two.tex
$ echo *
main.pdf main.tex one.pdf one.tex three.wow two.pdf two.tex
$ GLOBIGNORE='main.tex:main.pdf'
$ echo *
one.pdf one.tex three.wow two.pdf two.tex

GLOBIGNORE can be a list of expressions (colon-separated) that describe the names to ignore, as the above example shows. Your question was about excluding a pair of simple filenames, but you can use glob expressions too:

$ GLOBIGNORE='one.*:two.*'
$ echo *
main.pdf main.tex three.wow

The list of names matching the glob (* in these examples) is evaluated against the expressions in GLOBIGNORE. The names that match any of the expressions are removed from the list. It's a useful feature.

In many cases, it's also useful to use export, as in export GLOBIGNORE='main.tex:main.pdf'.

0

Whenever I run into a situation like this, I do it with a pipe:

ls -1 main.* | egrep -v '\.pdf|\.tex' | xargs rm

3
  • That will break horribly for file names containing spaces Or file names matching names in directories matching the glob main.*. Nov 3, 2022 at 21:36
  • Personally, I avoid spaces in file names, but if there were, you could insert a pipe with a sed to add in backslashes. Other go-arounds if there happens to be a directory named e.g. "main.directory" , for example "ls -1" => "ls -1dF | egrep -v '\/ *$' | sed -e's/\* *$//g' -e's/ /\\ /g' . . . Haven't tested this but usually I play around until it works and leave out the "xargs rm" while I am playing.
    – jmf7
    Nov 4, 2022 at 23:59
  • Ugly, for sure, but quick. (I MEANT TO WRITE: ls -1 main.* ==> ls -1dF main.*)
    – jmf7
    Nov 5, 2022 at 0:07

You must log in to answer this question.

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