5

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
4
  • 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, 2014 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, 2014 at 14:22
  • @cristian-ciupitu Thank you for improving my question.
    – Zettt
    Jun 15, 2014 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. Jun 16, 2014 at 1:01

6 Answers 6

7

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.

1
  • Note that for the last one, adding -delete is not enough. You need find Library/Caches \( ! -path '*Snapshots*' -o -type f -name Snapshots \) -delete Jun 15, 2014 at 16:48
6
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/.

2

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.

1
  • Snapshots isn't a direct subdirectory of Caches, and there are multiple Caches directories. A recursive traversal is needed. Jun 15, 2014 at 19:13
0
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"
}' \{\} \;
10
  • Snapshots isn't a direct subdirectory of Caches. Jun 15, 2014 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, 2014 at 19:42
  • Well, I think the downvote is fully justified in this case. It always makes my toenails roll up whenever I see cd in a shell script. This will cause nothing but trouble. Think of shell sessions, subshells, subshell A not knowing anything of subshell B and their parameters and vice versa---and so on. Avoid. Aug 30, 2014 at 18:19
  • @syntaxerror - what does A not knowing mean? And what is wrong with cd in a script anyway? It should be used in scripts - it is designed for scripting. Look into the $CDPATH environment variable and the effects of cd -P.
    – mikeserv
    Aug 30, 2014 at 18:26
  • 1
    @syntaxerror have a look here for a better example of what might be done with cd in a script. And also, the page you linked, is among the better of the wooledge stuff, though many of those so-called pitfalls fail entirely to discuss the whys which is what I dislike the most.
    – mikeserv
    Aug 30, 2014 at 20:55
0

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
-1

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.

4
  • 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, 2014 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, 2014 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, 2014 at 16:51
  • if to grep like this grep '\/Caches\/' and so on then your concern is also considered.
    – vadimbog
    Jun 14, 2014 at 16:54

You must log in to answer this question.

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