Take the 2-minute tour ×
Unix & Linux Stack Exchange is a question and answer site for users of Linux, FreeBSD and other Un*x-like operating systems.. It's 100% free, no registration required.

I'm having the following problem. I have a directory structure and want to delete everything that is within a directory named Caches. Everything should be delete except all directories named Snapshots. I can't figure out how to do this with find or any other command I know.

The reason I'm asking: On iOS every app has its own Caches directory. These sometimes don't get cleared properly, depending on the app. With the solution to this answer, one would be able to clear Caches on iOS, and therefore optimize the disk space, when the devices' drive is mounted on another computer, e.g. with FUSE (iExplorer).

This is what I have so far:

find . 2>/dev/null -type d -name "Caches" -maxdepth 3 -print

This returns something like:

./Library/Caches

When I do a ls ./Library/Caches I see all contents and the Snapshots directory, which I want to exclude because ultimately I want to -delete everything except this one.


I want something like this:

  Before:                            After:

  .                                  .
  ├── a                              ├── a
  │   ├── a                          │   ├── a
  │   └── Caches                     │   └── Caches
  │       ├── a                      │       └── a
  │       │   └── Snapshots          │           └── Snapshots
  │       │       └── a              │               └── a
  │       ├── b                      └── b
  │       │   └── a                      └── c
  │       └── c                  
  └── b
      ├── c
      └── Caches
          ├── a
          │   └── foo
          │       └── a
          └── b
              └── a
share|improve this question
    
Well, one (dirty?) trick would be to use GNU tar with the --remove-files and --exclude options, and then delete the tar files (as bonus, if you accidentally removed a file you didn't intend to, and notice it before deleting the tar file, you can simply restore it from that archive). –  celtschk Jun 14 at 13:34
    
What happens if you have a directory like ./Library/Caches/not_Snapshots/Caches/Snapshots/? Do you want to delete that directory because it is inside ./Library/Caches/not_Snapshots/ or save it because it is a Snapshots directory? –  jw013 Jun 14 at 14:22
    
@cristian-ciupitu Thank you for improving my question. –  Zettt Jun 15 at 18:49
    
@Zett, Stéphane Chazelas deserves the whole credit. I should have probably mentioned him in the question too, not just in the changelog. –  Cristian Ciupitu Jun 16 at 1:01

6 Answers 6

up vote 6 down vote accepted
find . -depth -print0 | perl -0lne '
  if ("$_/" =~ m{/Caches(/.*)}s && $1 !~ m{/Snapshots/}) {rmdir $_ or unlink $_}'

If your find doesn't support -print0, you can replace it with -exec printf '%s\0' {} +.

The idea is to print the list of files NUL-terminated (as 0 is the only byte that can't occur in a file path) and use perl's -n with -0 option to run some code for each of those filenames (with $_ set to the filename).

With -depth, files are printed before their parent directory. We remove only files or directories (assuming they are empty which is why it's important to process the list in depth order) if their path contains /Caches/ not followed by /Snapshosts/.

share|improve this answer

I am a little confused by wording of the question. If what you want to do is delete everything in the ./Library/Caches directory apart from a single folder called Snapshots, there is no need to use find. In bash, shell globs are the simplest way:

shopt -s extglob  # probably already enabled
echo Library/Caches/!(Snapshots)

If this prints all the files/directories that you want to delete, then replace echo by rm -f --.

If there are multiple Snapshots directories at different levels of the directory tree below ./Library/Caches that you want to preserve, then with GNU find you can do:

find Library/Caches ! -path '*Snapshots*'

This should print all files/directories excluding those that have Snapshots in their path. It will include directories that contain (or whose children contain) Snapshots directories, however find ... -delete will not delete those and instead print an error that they are not empty. Once you are happy, add -delete to the end.

One caveat is that this will leave any files named Snapshots intact. If this is a problem, instead do:

find Library/Caches \( ! -path '*Snapshots*' -o -type f -name Snapshots \)

Again add -delete when happy.

share|improve this answer
    
Note that for the last one, adding -delete is not enough. You need find Library/Caches \( ! -path '*Snapshots*' -o -type f -name Snapshots \) -delete –  Stéphane Chazelas Jun 15 at 16:48

I'm not a find sorcerer so I don't want to say that you can't do it with find, but I do know it can be done with a couple lines of bash. Given that I understand your directory structure correctly:

#!/bin/bash

for i in Library/Caches/*; do
    if [[ -d "$i" ]]; then
         [[ "$i" =~ "Snapshots" ]] ||  echo "rm-ing  $i" # change to rm -rf "$i" to use
    fi
done

The for statement returns each item in Library/Caches/. The if [[ -d "$i" ]] statement checks that each item is a directory, and if it is, then it's name is checked to see if it contains "Snapshots", [[ "$i" =~ "Snapshots" ]]. Because there isn't an operator to negate a regex match, like !=~, I used || to execute rm if the previous command is unsuccessful e.g. the directory name doesn't contain Snapshots.

This will run on any modern system using bash, but it must have bash. Also, you may need to play around with the directory structure, but that should do it for you.

share|improve this answer
    
Snapshots isn't a direct subdirectory of Caches, and there are multiple Caches directories. A recursive traversal is needed. –  Gilles Jun 15 at 19:13
cd ./Library/Caches
mv ./Snapshots ../    
sh -c 'rm -rf -- .[!.]* *' 
mv ../Snapshots ./

You don't need to set any persistent shell options. Just move the directory out, delete everything, then move it back in. I use the .[!.] glob above to handle any .files and put it within an sh statement because bash or zsh will likely not interpret the portable shell glob correctly. But if there aren't any .files you can use:

rm -rf -- *

..instead. Just don't do either in any directory in which you don't want everything deleted.

You can apply this same logic recursively - if there are multiple ./Caches/Snapshots instances, for example. First make a hold directory on the same filesystem as ./Library/:

mkdir -p hold ; hold=${PWD}/hold/Snapshots

Now find all Snapshots dirs, then save them elsewhere and delete the rest:

cd ./Library
find . -type d -name Snapshots -exec sh -c '
    [ "$0" != "${0%/Caches/Snapshots}" ] && {
    mv "$0" '"${hold}"' &&
    ( cd "${0%/*}" && rm -rf -- .[!.]* * ) && 
    mv '"${hold}"' "$0"
}' \{\} \;
share|improve this answer
    
Snapshots isn't a direct subdirectory of Caches. –  Gilles Jun 15 at 19:13
    
@Gilles - that picture was not there yesterday. Do you often downvote working answers because they dont meet stipulations added after the fact? –  mikeserv Jun 15 at 19:42

First part: find directories named caches and do something with them

find . -name Caches -type d -exec … \;

Second part: traverse a cache directory, and remove everything, but keep Snapshots subdirectories and their content (as well as paths leading to them). Do a depth-first traversal, so that directories can be deleted once they become empty.

    find /path/to/Caches -depth -name Snapshots -type d -prune \
                             -o \( -type d -empty -o ! -type d \) -delete

Now combine them together.

find . -name Caches -type d -exec
    find {} -depth -name Snapshots -type d -prune \
                -o \( -type d -empty -o ! -type d \) -delete
share|improve this answer

Why just not to find what you need by excluding what you want to keep?

Look for files rather than for directories:

find . -type f | grep Cashes | grep -v Snapshot | xargs rm

So you will find all files from current directories then you'll select only those files which are in directories Cashes then you exclude those files which are in directory Snapshot and then you remove the rest.

Above might leave empty directories but you could play with find options to get needed result.

share|improve this answer
1  
That will also match oldCaches and badSnapshot etc. It will also break on the (unlikely) case of filenames with newlines and the (much likelier) case of filenames with spaces. It will also fail on cases where you have files whose name starts with a -. –  terdon Jun 14 at 16:06
    
should you have file names with spaces then a slight change in above will solve this issue as well ` find . -type ... | sed 's/^/"/;s/$/"/' | xargs -ifile rm file` –  vadimbog Jun 14 at 16:45
    
Yes, but that will break on, for example, files like foo bar" baz. If you want to make it really safe, you need find . -type f -print0 | xargs -0 rm or similar constructs. In any case, this does not help the main problem which is that you are not checking in the right places since grep Caches will match any file whose name or path contains Caches anywhere. –  terdon Jun 14 at 16:51
    
if to grep like this grep '\/Caches\/' and so on then your concern is also considered. –  vadimbog Jun 14 at 16:54

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.