Take the 2-minute tour ×
Super User is a question and answer site for computer enthusiasts and power users. It's 100% free, no registration required.

The problem

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

./A
./B/A/A
./B/A/B
./B/B/
./B/C
./C/

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

./A
./B/A

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

./A
./B/A/A
./B/A/B

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
.
./B
./B/B
./B/C
./C

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 something-that-calls-find.sh) since the directory lists I'll be handling are quite big...

share|improve this question

2 Answers 2

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:

./B/B
./B/C
./C

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.

Code

#!/bin/bash

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

    for arg; do
        CURRENT_CHUNK="$arg"

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

## recursively removes all files in given directory, except given names.
# $1: target directory
# $2...: exceptions
function remove_recursive() {
    local DIR="$1"
    shift
    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 )
    done

    # ...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
}

Explanation

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

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

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