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?


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 :) – Lee May 17 '16 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 .. – Lee May 17 '16 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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.