56

I need to delete all folders inside a folder using a daily script. The folder for that day needs to be left.

Folder 'myfolder' has 3 sub folder: 'test1', 'test2' and 'test3' I need to delete all except 'test2'.

I am trying to match exact name here:

find /home/myfolder -type d ! -name 'test2' | xargs rm -rf

OR

find /home/myfolder -type d ! -name 'test2' -delete

This command always tries to delete the main folder 'myfolder' also ! Is there a way to avoid this ?

2
  • 8
    In Unix and Linux, we call these "directories", not "folders". Commented Feb 7, 2018 at 2:34
  • 1
    Depending on your shell, you may need to quote that ! operator: \! or '!'. Commented Feb 7, 2018 at 8:32

7 Answers 7

69

This will delete all folders inside ./myfolder except that ./myfolder/test2 and all its contents will be preserved:

find ./myfolder -mindepth 1 ! -regex '^./myfolder/test2\(/.*\)?' -delete

How it works

  • find starts a find command.
  • ./myfolder tells find to start with the directory ./myfolder and its contents.

  • -mindepth 1 not to match ./myfolder itself, just the files and directories under it.

  • ! -regex '^./myfolder/test2\(/.*\)?' tells find to exclude (!) any file or directory matching the regular expression ^./myfolder/test2\(/.*\)?. ^ matches the start of the path name. The expression (/.*\)? matches either (a) a slash followed by anything or (b) nothing at all.

  • -delete tells find to delete the matching (that is, non-excluded) files.

Example

Consider a directory structure that looks like;

$ find ./myfolder
./myfolder
./myfolder/test1
./myfolder/test1/dir1
./myfolder/test1/dir1/test2
./myfolder/test1/dir1/test2/file4
./myfolder/test1/file1
./myfolder/test3
./myfolder/test3/file3
./myfolder/test2
./myfolder/test2/file2
./myfolder/test2/dir2

We can run the find command (without -delete) to see what it matches:

$ find ./myfolder -mindepth 1 ! -regex '^./myfolder/test2\(/.*\)?'
./myfolder/test1
./myfolder/test1/dir1
./myfolder/test1/dir1/test2
./myfolder/test1/dir1/test2/file4
./myfolder/test1/file1
./myfolder/test3
./myfolder/test3/file3

We can verify that this worked by looking at the files which remain:

$ find ./myfolder
./myfolder
./myfolder/test2
./myfolder/test2/file2
./myfolder/test2/dir2
4
  • 1
    Alternative to -prune to leave test2/*/ subdirectories alone: return to rm -r and add -maxdepth 1. Commented Feb 7, 2018 at 8:35
  • @Isaac OK. Done. (Also, +1 for your excellent answer.) Commented Nov 9, 2018 at 8:30
  • 1
    Excellent work!, but sorry: that will remove all files inside ./myfolder. You need a missing (IMvhO) -type d for only directories. Commented Nov 9, 2018 at 9:06
  • 1
    Ok, this should work as you want: find ./myfolder -depth -mindepth 1 -maxdepth 1 -type d ! -regex '^./myfolder/test2\(/.*\)?' Commented Nov 9, 2018 at 10:25
21

Using bash:

shopt -s extglob
rm -r myfolder/!(test2)/

Example:

$ tree myfolder/
myfolder/
├── test1
│   └── file1
├── test2
│   └── file2
└── test3
    └── file3

$ echo rm -r myfolder/!(test2)
rm -r myfolder/test1 myfolder/test3
$ rm -r myfolder/!(test2)
$ tree myfolder/
myfolder/
└── test2
    └── file2

1 directory, 1 file
1
  • 2
    awesome, 20 years using bash I never wondered rm could get some sort of regex! Commented Jun 29, 2022 at 10:24
13

tl;dr

find ./myfolder -mindepth 1 -maxdepth 1 -type d -not -name test2 \
     -exec echo rm -rf '{}' \;

Remove echo if satisfied with the list of files.


Using -mindepth 1 will ensure that the top directory is not selected.

$ find ./myfolder -mindepth 1 -type d
./myfolder/test2
./myfolder/test2/one
./myfolder/test2/two
./myfolder/test
./myfolder/test/a1
./myfolder/test/a1/a2
./myfolder/test/a1/a2/a3

But a -not -name test2 will not avoid subdirs inside test2:

$ find ./myfolder -mindepth 1 -type d -not -name 'test2'
./myfolder/test2/one
./myfolder/test2/two
./myfolder/test
./myfolder/test/a1
./myfolder/test/a1/a2
./myfolder/test/a1/a2/a3

To do that, you need something like prune:

$ find ./myfolder -mindepth 1 -name test2 -prune -o -type d -print
./myfolder/test
./myfolder/test/a1
./myfolder/test/a1/a2
./myfolder/test/a1/a2/a3

But do not use delete, as it implies depth and that will start erasing from the longest path:

$ find ./myfolder -depth -mindepth 1 -name test2 -prune -o -type d -print
./myfolder/test/a1/a2/a3
./myfolder/test/a1/a2
./myfolder/test/a1
./myfolder/test

Either use rm -rf (remove the echo if you want to actually erase):

$ find ./myfolder -mindepth 1 -name test2 -prune -o -type d -exec echo rm -rf '{}' \;
rm -rf ./myfolder/test
rm -rf ./myfolder/test/a1
rm -rf ./myfolder/test/a1/a2
rm -rf ./myfolder/test/a1/a2/a3

Or, also use maxdepth if all you need is to delete directories (and everything inside) (remove the echo to actually erase):

$ find ./myfolder -mindepth 1 -maxdepth 1 -type d -not -name test2 -exec echo rm -rf '{}' \;
rm -rf ./myfolder/test

A -delete will still fail if the directory is not empty:

$ find ./myfolder -mindepth 1 -maxdepth 1 -type d -not -name test2 -delete
find: cannot delete ‘./myfolder/test’: Directory not empty
0
4

If you're using zsh, then you could:

setopt extended_glob # if you don't have it enabled

rm -rf myfolder/^test2(/)

^foo is a glob meaning everything except foo, and (/) is a glob qualifier that says that the glob should only match directories.

3

If the requirement is only for level one (not more than depth 1) as mentioned in the question, here is another easy option:

ls myfolder/ | grep -v "test2" | xargs rm -r

Details:

  • ls myfolder/ lists all folders and files inside myfolder/ directory
  • grep -v "test2" matches everything except "test2"
  • xargs rm -r pipes the output from previous command to rm -r command.
1
  • And with recursive delete of all sub folders find myfolder/* -type d | grep -v "test2" | xargs rm -rf Commented Apr 23, 2021 at 9:12
1

Tested with below command and it worked fine

find  /home/myfolder -maxdepth 1 -type d ! -iname test2 -exec rm -rvf {} \;
1
  • 3
    You've run into the same problem as the OP; listing /home/folder on the command-line (without the critical -mindepth 1) makes that top directory match all the criteria (it's a directory and it's not named "test2") and so it gets deleted. Commented Nov 8, 2018 at 20:21
0

for one file/directory ("createwrapper" here):

rm -rf `ls -A | grep -xv createwrapper`

for two file names ("createwraper" and "gradle"):

rm -rf `ls -A | grep -xv '\(createwrapper\|gradle\)'`

note that you need to backslash-escape dots, otherwise .gradle will match xgradle too:

rm -rf `ls -A | grep -xv '\(createwrapper\|\.gradle\)'`

Explanation:

ls -A shows "almost all" files, that is, it shows hidden files but not . and ..

|, the pipe symbol, directs the standard output of one command to the standard input of the other command

ls -A | cat prints one filename per line (here cat just outputs what it gets, it is ls that knows that it outputs to a pipe)

grep -x means "exactly match the whole line"

grep -v means "invert condition"

rm -rf means "remove recursively and force (do not ask)"

rm -rf `some-command(s)` means "get what some-command(s) print and pass it to rm -rf"

'\(createwrapper\|\.gradle\)' is a regular expression that matches either createwrapper or .gradle. Basically, what is meant is (createwrapper|.gradle) with | meaning "or", but there are some problems; with regular expressions there always are some problems.

ls -A --hide=createwrapper does not work: ls --hide=createwrapper does not show hidden files, and ls -A --hide=createwrapper works just like ls -A.

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.