Basically, given a parent directory (PARENT), containing 1..M directories (DIRA-DIRZ), that each contain 0..N directories (DIR1-DIRN), that each contain a list of files (FILEa-FILEz)...


... if the second level directory (DIR1-DIRN) exists, then move the files from the second level directory (DIR1) into the first level directory that contains it (DIRA), and then remove the now empty second level directory (DIR1).


I found this snippet that I'm trying to work from:

find /thisdir -type f -name "*.ogg" -exec mv {} /somedir \;

but I can't work out the outer loop and how to integrate them.

Any help that you can give, especially with some explanation, would be outstanding!

  • 1
    What have you tried and where is it not working? Add what is there now and the expected output and details. Right now, it's hard to distinguish which ones is who or what is desired. Dec 25, 2020 at 19:07
  • I found this snippet that I'm trying to work from: find /thisdir -type f -name "*.ogg" -exec mv {} /somedir \; but I can't work out the outer loop and how to integrate them. I'm very new to this and I'm getting really lost.
    – aybesea
    Dec 25, 2020 at 20:08
  • 2
    so you are losing one file (or more if there are multiple files with same name) as result of PARENT/DIRC/DIR2/files and PARENT/DIRC/DIR3/files movement into PARENT/DIRC/files! Are you OK with that?! Dec 25, 2020 at 21:58

4 Answers 4


Since nearly all Linux distros -- at least the major ones -- come with Python pre-installed, probably it would be easier to just write a simple Python script instead.

#!/usr/bin/env python3
import sys
from pathlib import Path

def flatten_and_del(targdir):
    pardir = targdir.parent
    for f in targdir.glob("*"):
        f.rename(pardir / f.name)

def process_dir(parent):
    parent = Path(parent)
    if not parent.is_dir():
        raise RuntimeError(f"{parent} is not a directory!")
    for f in parent.glob("*"):
        if not f.is_dir():

if __name__ == "__main__":

Save it as, say, tidy-dirs.py, and run it like so:

python3 tidy-dirs.py path/to/PARENT

WARNING: As mentioned by αғsнιη in their comment, this method will cause data loss if the filename conflicts. If you don't want that to happen, in flatten_and_del, add target.exists() check before doing .rename()

  • 1
    @alecxs Naah, my method above won't "fail", Path.rename() will silently overwrite already existing file. Will cause data loss in αғsнιη's scenario, but won't fail 😉
    – pepoluan
    Jan 7, 2021 at 16:14

With zsh:

autoload zmv # best in ~/.zshrc
zmv '(PARENT)/(*)/*/(*)(#qD)' '$1/$2/$3'
rmdir PARENT/*/*(#qD/^F)

actually that are two commands. first is mv the files one level up, second is delete empty directories.
(both commands are concatenated with && to make it look like one-liner)

find -H PARENT -mindepth 3 ! -type d -execdir cp --backup=numbered -al {} .. \; -delete && find -H PARENT -depth -mindepth 2 -type d -exec rmdir {} +

instead of mv create hard links and delete the origin. cp provides build-in backup solution:

cp --backup=numbered will auto rename file in case another file already exist
cp -l create hard link instead of copy
cp -a preserve metadata (mtime, permissions)
find -delete deletes everything recursively (use with caution)

all find arguments explained:

find -H in case PARENT is a symlink
find ! -type d same as -type f (! = invert argument)
find -mindepth 3 only */*/*/files at 3rd level (use -maxdepth 3 to make it more safe)
find -execdir run command from subdirectory

difference between \; and + explained:

find -exec CMD {} \; serial 1:1 execution of CMD ARG1; CMD ARG2; ... (where ARG is the result of find)
find -exec CMD {} + parallel execution of CMD ARG1 ARG2 ARG3 ... ARGN;

instead of running from terminal you can replace PARENT with "$@" and create a little shell script which will run on multiple arguments.

sh ./myscript.sh PARENT [PARENT2...] run the script

test -e "$1" || exit 1
find -H "$@" -mindepth 3 ! -type d -execdir cp --backup=numbered -al {} .. \; -delete
find -H "$@" -depth -mindepth 2 -type d -exec rmdir {} +

This can be easily resolved with a simple bash script to save time if you have a lot of directories to do this.

With just one dir:

└── DIRA
    └── DIR1
        ├── file1.ogg
        ├── file2.ogg
        └── file3.ogg

Then, inside DIR1:

# You can also use ; over && but the second one is more reliable,
# specially if you are doing this in a remote server.

mv *.ogg ../ && cd ../ && rmdir DIR1/


└── DIRA
    ├── file1.ogg
    ├── file2.ogg
    └── file3.ogg

If you are certain that all files inside the second level directory are .ogg, you can use mv * instead of mv *.ogg.

  • you really want OP to cd into each DIRA-Z and DIR1-N manually? what if N=99
    – alecxs
    Jan 7, 2021 at 8:12
  • as I said, for a lot of dirs, a bash script is the best solution, but this three commands is the core of the script
    – dsenese
    Jan 7, 2021 at 22:32
  • ... and you think OP has asked this question about find because he did not know about usage of such very basic core utils cd mv rmdir and wildcard * or do you plan to edit your answer and provide a working solution?
    – alecxs
    Jan 7, 2021 at 23:16

You must log in to answer this question.

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