I want to keep files whose names match [0-9A-Z]{1,2}_\d{4}_\w+?\.dat, for example, A1_2001_pm23aD.dat, K_1998_12.dat, and remove the rest.

However, the ls and rm commands do not support such regexes. How can I do this?

3 Answers 3


Using extended globs:

shopt -s extglob
printf '%s\n' !([[:digit:][:upper:]]?([[:digit:][:upper:]])_[[:digit:]][[:digit:]][[:digit:]][[:digit:]]_+([[:alnum:]]).dat)

this will print all file/directory names that do not (!) match [[:digit:][:upper:]] followed by zero or one [[:digit:][:upper:]] followed by 4 [[:digit:]] in between _s and then one or more [[:alnum:]] before the extension .dat.
If you want to search recursively:

shopt -s globstar
shopt -s extglob
printf '%s\n' **/!([[:digit:][:upper:]]?([[:digit:][:upper:]])_[[:digit:]][[:digit:]][[:digit:]][[:digit:]]_+([[:alnum:]]).dat)

Alternatively, with gnu find (you can use a regex):

find . -regextype egrep ! -regex '.*/[[:digit:][:upper:]]{1,2}_[[:digit:]]{4}_[[:alnum:]]+\.dat$'
  • 1
    look good except the terribly looong regex :)
    – wsdzbm
    May 17, 2016 at 15:36

There are many ways of doing this. You could use a scripting language that understands regular expressions. For example, in Perl:

perl -le 'unlink(grep(!/[0-9A-Z]{1,2}_\d{4}_\w+?.dat/,@ARGV))' *

That will look for all files (not subdirectories) in the current directory, collect those that don't match the regex and delete them.

You could also do a similar thing with bash, you just need to translate the regex to POSIX ERE:

for f in *; do 
    [[ "$f" =~ [0-9A-Z]{1,2}_[0-9]{4}_[a-zA-Z0-9]+.dat ]] || rm "$f"; 

Note that in your regex, \w+?.dat will try to match the smallest possible alphanumeric string any character and dat. I don't see why you would want to use +? here and you probably meant to use \.dat. I am guessing you also probably want to make sure the entire file name matches, so that things like foobarfoobarfoobarA1_2001_pm23aD.datfoobarfooabr are also removed. If so, use one of these instead:

perl -le 'unlink(grep(!/^[0-9A-Z]{1,2}_\d{4}_\w+\.dat$/,@ARGV))' *


for f in *; do 
    [[ "$f" =~ ^[0-9A-Z]{1,2}_[0-9]{4}_[a-zA-Z0-9]+.dat$ ]] || rm "$f"; 

Finally, to also delete directories, you could do:

for f in *; do 
    [[ "$f" =~ ^[0-9A-Z]{1,2}_[0-9]{4}_[a-zA-Z0-9]+.dat$ ]] || rm -rf "$f"; 
  • yes, you are correct. I forgot to escape ..
    – wsdzbm
    May 17, 2016 at 14:29

You can do it with find:

find . -regextype posix-extended \
            -type f ! -regex '.*/[0-9A-Z]{1,2}_[[:digit:]]{4}_[[:alnum:]_]+?\.dat' -delete
  • Of course you can put it all on one line (removing the \ at the end of the first line).
  • -regextype posix-egrep seems to work exactly as well as -regextype posix-extended.
  • If your version of find doesn't support -delete, use -exec rm -- {} + or -exec rm -- {} ';'.
  • If you want to search only the top-level directory, use -maxdepth 1.

You must log in to answer this question.

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