2

Is it possible to do the below with one line?

shopt -s extglob

rm -r !(folder1|file1|folder2)
rm -r folder2/!(subfolder)
rm -r folder2/subfolder/!(onefile)

For example, something like this?

rm -r !(folder1|file1|folder2/subfolder/onefile)
1

You can use the (negated) -path test of the find tool. If the tool supports -delete then it's relatively straightforward:

find . ! -path ./folder1 ! -path './folder1/*' ! -path ./file1 ! -path ./folder2/subfolder/onefile -delete

If onefile exists then subfolder and folder2 will be reported in cannot delete: Directory not empty errors. This will make them survive but the exit status from find will not be 0.

If your find supports -empty then you can test directories for being empty before you -delete them. In general you don't want to test non-directories for being empty. This complicates the command but allows it to return 0 if everything goes as planned. Like this:

find . ! -path ./folder1 \
       ! -path './folder1/*' \
       ! -path ./file1 \
       ! -path ./folder2/subfolder/onefile \
       \( ! -type d -o -empty \) \
       -delete

find rich enough to support -delete and -empty will probably support -regex. GNU find in my Debian can do this:

find . ! -regex '\./folder1/*.*\|\./file1\|\./folder2/subfolder/onefile' -delete

Notes:

  • It's the existence of onefile what prevents find from deleting subfolder or folder2. You need more patterns (see down below) or more complicated regex to keep these directories regardless of whether onefile is there or not. A variant with more complicated regex:

    find . ! -regex '\./folder1/*.*\|\./file1\|\./folder2\(/subfolder\(/onefile\)?\)?' -delete
    

    As a bonus such improvement fixes the "non-zero exit status" issue.

  • -path uses glob-like pattern matching notation (? matches a single character and * matches zero or more characters); -regex uses regular expressions (. matches a single character and .* matches zero or more characters).

  • -path and -regex match against the entire path of a file. To craft patterns that work, you need to know what path find will use. Hints:

    • find is obliged not to use trailing slash for any path that is not a starting point. This is the reason I used separate -path patterns for ./folder1 and files within (./folder1/*).
    • On the other hand starting points are used as provided. If the starting point is . then all paths will start with ./ except for the path to . itself, it will be just .. If the starting point is ./ then all paths will start with ./.
  • In my tests -delete silently processes . without deleting and without error, so there is no need to exclude .. On the other hand it throws an error for ./. I'm not sure how reliable this behavior is. In case of any doubt explicitly exclude ., especially if you're after a meaningful exit status.

  • There are few regex types. E.g. I could use -regextype posix-extended before ! -regex. The following is the most compact command I came up with (not portable though):

    find . -regextype posix-extended \
         ! -regex './folder1(/.*)?|./file1|./folder2(/subfolder(/onefile)?)?' \
           -delete
    

    I intentionally used . (wildcard) instead of \. (literal dot) for the leading .. Since the starting point is ., every path will start with . for sure, so the wildcard will match nothing else than .. Use \. if you prefer formal correctness.

  • If you want to make sure you don't delete to much, you can "dry run" the command. Change -delete to -print, you will see what would be deleted. This is the very first command modified this way:

    find . ! -path ./folder1 ! -path './folder1/*' ! -path ./file1 ! -path ./folder2/subfolder/onefile -print
    

    Or negate all preceding tests as a whole (which is not the same as negating them separately unless you know what more you need to change; compare this to this) and change -delete to -print to see what would survive:

    find . ! \( ! -path ./folder1 ! -path './folder1/*' ! -path ./file1 ! -path ./folder2/subfolder/onefile \) -print
    
  • Unquoted ./folder1/* would be a bug: the shell would try to expand *.


-path is required by POSIX but -delete, -regex and -empty are not. Sometimes -exec rm {} + can be used instead of -delete, but such rm will not remove directories, you need rmdir. Yes, rm -r can remove directories but then rm -r ./folder2 will also remove ./folder2/subfolder/onefile and defy the whole point.

Still our basic approach can be implemented without -delete (and somewhat improved):

find . -depth \
     ! -path  . \
     ! -path  ./folder1 \
     ! -path './folder1/*' \
     ! -path  ./file1 \
     ! -path  ./folder2 \
     ! -path  ./folder2/subfolder \
     ! -path  ./folder2/subfolder/onefile \
       \( \( ! -type d -exec rm {} + \) -o \( -type d -exec rmdir {} + \) \)

By explicitly excluding ./folder2 and ./folder2/subfolder I made the command return meaningful exit status and keep these directories even if onefile doesn't exist.

If you'd like to remove the directories if onefile doesn't exist then don't use the extra patterns. Research the --ignore-fail-on-non-empty option of rmdir. This option is not required by POSIX.

I won't say this last command is compact. I like the one with -regex better.

1
  • Kudos! That's quite sophisticated. I also like the regex method most. I ended up with the following: A) find . -regextype awk ! -regex "./folder1/.*|./file1|./folder2/subfolder/(fileone|filetwo)" -delete &> /dev/null, if non-zero exit code is acceptable. B) find . -type f | grep -Pv "folder1/.*|file1|folder2/subfolder/(fileone|filetwo)" | xargs -d '\n' rm and then find . -type d -empty -delete, which will remove all empty directories; if the slower processing isn't an issue for you. – Klaidonis Feb 25 '20 at 14:38
1

For such a potentially destructive operation I'd do it carefully, by hand: get a list of all files, with full paths; edit out the files that should stay (grep -v, or with your favorite text editor); check that nothing slipped through; give the list to rm (might need to use xargs(1) if the list is huge). Be careful with file names containing spaces or other unusual characters!

1
  • Thanks for the advice. However, regex is still necessary; thus, you have to craft it very carefully. – Klaidonis Feb 25 '20 at 14:40

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.