Super User is a question and answer site for computer enthusiasts and power users. Join them; it only takes a minute:

Sign up
Here's how it works:
  1. Anybody can ask a question
  2. Anybody can answer
  3. The best answers are voted up and rise to the top

The problem

I have a directory structure, e. g. something like this (here trailing slashes represent folders):


And I need to recursively remove everything except some files and directories:


After executing the command/script I'm searching for, I want to get the following hierarchy remaining:


The attempted solution

I tried to use find (-print is a placeholder):

find \( -path ./A -or -path ./B/A \) -prune -or -print

This does not work because it removes parent directories of entries in the "do not touch" list:

$ find \( -path ./A -or -path ./B/A \) -prune -or -print

Especially, this removes ./B while I need to keep ./B/A. Heck, it removes current directory, after all.

I want to avoid recursive invocations (i. e. find -exec since the directory lists I'll be handling are quite big...

share|improve this question
up vote 1 down vote accepted

I think it's easiest to use a regular expression to match the paths

  • ./B/A
  • ./B/A/A
  • ./B/A/B
  • ./B/A/B/C
  • et cetera

So, the following will match ./A or anything below the ./B/A folder, including it. I added a \ to make the command more readable. Also note that this only works with GNU find, i.e. not on BSD find.

find -depth -regextype posix-extended -mindepth 1 \
! \( -path "./A" -or -regex "\./B(/A(/.*)?)?" \)

To explain the regex: The /.* matches anything under the A directory. You need the slash here because otherwise a directory called AB would have been matched too. This previous pattern can appear zero times (for the directory A) or once (for anything below A), that's why we need the ?. Since we don't want to delete B, the part after it can occur zero or once (?).

Since there's a negation (!), the find command would match:


You could then add the -exec rm -rf {} option to remove those files and folders. We need the -depth option to start with the deepest one though, so as not to try and remove folders that don't exist anymore.

share|improve this answer
Here is why it will not work: it will try to remove ./B, and thus will remove ./B/A despite it is in the "ignore" list. – intelfx Sep 4 '13 at 4:34
True, I totally missed that. I updated my regex. – slhck Sep 4 '13 at 6:15
Then yes, it seems to work and seems scriptable. However, 1) it's better to use -exec rm -rf {} \; -prune and remove .* part of the regex, at least performance-wise; 2) I've found an easier-to-script solution (no path->regex transformation); I'll post it shortly. – intelfx Sep 4 '13 at 11:35
Cool, would be nice to see what you've come up with! I didn't have much time to improve and iterate on this. – slhck Sep 4 '13 at 11:50
done, and my solution is two functions vs. one line :) – intelfx Sep 4 '13 at 12:49

Here's my own solution to that.
NOTE: I'm not that much of a portability lover when it comes to shell and utilities, so it possibly heavily depends on Bash 4 and GNU find.



## given "a/b/c/d", prints "a/b/c", "a/b" and "a".
# $1...: pathes to process
function get_parent_directories() {

    for arg; do

        while true; do
            CURRENT_CHUNK="$(dirname "$arg")"
            [[ "$CURRENT_CHUNK" == "." ]] && break
            echo "$CURRENT_CHUNK"

## recursively removes all files in given directory, except given names.
# $1: target directory
# $2...: exceptions
function remove_recursive() {
    local DIR="$1"
    local EXCEPTIONS=( "$@" )

    # find all files in given directory...
    local FIND_ARGS=( find "$DIR" -mindepth 1 )

    # ...skipping all exceptions and below...
    for file in "${EXCEPTIONS[@]}"; do
        FIND_ARGS+=( -path "$file" -prune -or )

    # ...and ignoring all parent directories of exceptions (to avoid removing "./B" when "./B/A" is an exception)...
    while read file; do
        FIND_ARGS+=( -path "$file" -or )
    done < <(get_parent_directories "${EXCEPTIONS[@]}" | sort -u)

    # ...and printing all remaining names, without their descendants (we're going to recursively remove these anyway).
    FIND_ARGS+=( -print0 -prune )

    "${FIND_ARGS[@]}" | xargs -r0 rm -r


The resulting find command line is built as a chain of -predicates -actions -or sequences.

This means following: for each path, if -predicates succeed, do -actions, otherwise go on to the next sequence. Last element in the chain is just -actions, which is the default case.

Here, I'm doing -prune for all pathes directly found in $EXCEPTIONS. This stops find from descending beyond these names.

Next, I'm doing nothing for all parents of pathes in $EXCEPTIONS. We do not want to remove parent directories of exceptions, as the removal is recursive.

Finally, I'm feeding all remaining pathes (the default case) to xargs rm -r. This is just faster than -exec rm -r {} \; because only one rm will be spawned.

I also do -prune for them because there is no point in explicitly removing ./A/B/C if we're going to remove ./A/B.

P. S.: this ended up in my snippet library :)

share|improve this answer

You must log in to answer this question.

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