120

This is probably very simple, but I can't figure it out. I have a directory structure like this (dir2 is inside dir1):

/dir1
    /dir2 
       |
        --- file1
       |
        --- file2

What is the best way to 'flatten' this directory structure in such a way as to get file1 and file2 into dir1 not dir2?

2

7 Answers 7

128

You can do this with GNU find and GNU mv:

find /dir1 -mindepth 2 -type f -exec mv -t /dir1 -i '{}' +

Basically, the way that works if that find goes through the entire directory tree and for each file (-type f) that is not in the top-level directory (-mindepth 2), it runs a mv to move it to the directory you want (-exec mv … +). The -t argument to mv lets you specify the destination directory first, which is needed because the + form of -exec puts all the source locations at the end of the command. The -i makes mv ask before overwriting any duplicates; you can substitute -f to overwrite them without asking (or -n to not ask or overwrite).

As Stephane Chazelas points out, the above only works with GNU tools (which are standard on Linux, but not most other systems). The following is somewhat slower (because it invokes mv multiple times) but much more universal:

find /dir1 -mindepth 2 -type f -exec mv -i '{}' /dir1 ';'

POSIXly, passing more than one argument, but using sh to reorder the list of arguments for mv so the target directory comes last:

LC_ALL=C find /dir1 -path '/dir1/*/*' -type f -exec sh -c '
  exec mv "$@" /dir1' sh {} +
15
  • 4
    Edited to use -exec + so that it doesn't execute a large number of processes of mv
    – Random832
    Commented Oct 24, 2012 at 19:32
  • 2
    @Random832 And going to revert again, because + doesn't work. mv needs the destination as the final argument, but + would have the sources as the final argument. Find wont even accept the syntax you changed it to (find: missing argument to `-exec')
    – derobert
    Commented Oct 24, 2012 at 19:34
  • 2
    @Random832 but I suppose mv has a -t we can use, so I'll change it to that.
    – derobert
    Commented Oct 24, 2012 at 19:36
  • 3
    Or find ./dir -mindepth 2 -type f -exec mv -f '{}' ./dir ';' if overriding duplicates
    – Atav32
    Commented Jan 22, 2019 at 17:04
  • 2
    @a--m {} is replaced by find with the file name/path.
    – derobert
    Commented Sep 7, 2019 at 20:02
66

In zsh:

mv dir1/*/**/*(.D) dir1

**/ traverses subdirectories recursively. The glob qualifier . matches regular files only, and D ensures that dot files are included (by default, files whose name starts with a . are excluded from wildcard matches). To clean up now-empty directories afterwards, run rmdir dir1/**/*(/Dod)/ restricts to directories, and od orders the matches depth first so as to remove dir1/dir2/dir3 before dir1/dir2.

If the total length of the file names is very large, you may run into a limitation on the command line length. Zsh has builtins for mv and rmdir which are not affected by this limitation: run zmodload zsh/files to enable them.

With only POSIX tools:

find dir1 -type f -exec mv {} dir1 \;
find dir1 -depth -exec rmdir {} \;

or (faster because it doesn't have to run a separate process for each file)

find dir1 -type f -exec sh -c 'mv "$@" dir1' _ {} +
find dir1 -depth -exec rmdir {} +
4
  • 2
    This should be the accepted answer! Especially with the concise zsh version.
    – Adamski
    Commented Sep 6, 2017 at 10:18
  • 1
    The zsh option also works in bash if you have shopt -s globstar in your .bashrc. So you can just mv ./**/*.* ./ and then rm -r <directories>. Commented Aug 19, 2020 at 22:34
  • @JollyLlama No, that doesn't do the same thing. **/*(.D) in zsh only matches regular files. There's no equivalent in bash. **/*.* matches directories as well. And for whatever reason you excluded files with no . in their name, which you delete rather than move. Commented Aug 20, 2020 at 7:53
  • You’re right, I didn’t realize I was making the (false) assumption that we were dealing with user files and folders, so I assumed that files will all be of the form file.ext and folders will all be extensionless. What I did works if those assumptions hold, but not in general. Commented Aug 20, 2020 at 16:43
10

Expanding on the popular answer for this question, since I had a use-case for flattening a directory containing files of the same name.

dir1/
├── dir2
│   └── file
└── dir3
    └── file

In this case, the -i (--interactive) option passed to mv wouldn't yield the desired result to flatten the directory structure and handle name conflicts. So it's simply replaced with --backup=t (equivalent to --backup=numbered). More documentation on the -b (--backup) option available at https://www.gnu.org/software/coreutils/manual/coreutils.html#Backup-options.

Resulting in:

find dir1/ -mindepth 2 -type f -exec mv -t dir1/ --backup=t '{}' +

Which yields:

dir1/
├── dir2
├── dir3
├── file
└── file.~1~
1
  • 1
    is there anyway to make file~1~.extension, otherwise got file.extension.~1~ when duplicated, so had to rename that manually to be able to preview Commented Apr 8, 2022 at 8:14
10

tar and zip both have the ability to incorporate and then strip away a directory structure, so I was able to quickly flatten a nested directory with

tar -cvf all.tar *

followed by moving all.tar to a new location then

tar -xvf all.tar --strip=4

2
  • This is a good point, and one I hadn't thought of. You could even just do this with a pair of tar's connected by a pipe. Something like (haven't tried it) tar -c * | tar -x --strip 4
    – swestrup
    Commented Mar 28, 2020 at 15:37
  • 2
    Too much overhead Commented Sep 7, 2021 at 16:45
5

Try doing this :

cp /dir1/dir2/file{1,2} /another/place

or for each files matching file[0-9]* in the subdir :

cp /dir1/dir2/file[0-9]* /another/place

See http://mywiki.wooledge.org/glob

3
  • I should have indicated this, but I have to many files to use {} in my real problem.
    – turtle
    Commented Oct 24, 2012 at 19:30
  • See my second solution Commented Oct 24, 2012 at 19:31
  • Bingo. Thanks for the help. This is definitely the best solution.
    – turtle
    Commented Oct 24, 2012 at 19:34
4

Staying in dir1 you can just do it with mv dir2/* .. If you have more subfolders on dir2 level you can use mv */* .

2

I wrote two functions you can use together that do just that, you can limit the directory level by adding a -maxdepth $VAL parameter.

# This scripts flattens the file directory
# Run this script with a folder as parameter:
# $ path/to/script path/to/folder

#!/bin/bash

rmEmptyDirs(){
    local DIR="$1"
    for dir in "$DIR"/*/
    do
        [ -d "${dir}" ] || continue # if not a directory, skip
        dir=${dir%*/}
        if [ "$(ls -A "$dir")" ]; then
            rmEmptyDirs "$dir"
        else
            rmdir "$dir"
        fi
    done
    if [ "$(ls -A "$DIR")" ]; then
        rmEmptyDirs "$DIR"
    fi
}

flattenDir(){
    local DIR="$1"
    find "$DIR" -mindepth 2 -type f -exec mv -i '{}' "$DIR" ';'
}

read -p "Do you wish to flatten folder: ${1}? " -n 1 -r
echo    # (optional) move to a new line
if [[ $REPLY =~ ^[Yy]$ ]]
then
    flattenDir "$1" &
    rmEmptyDirs "$1" &
    echo "Done";
fi
3
  • 2
    Man, I just misused your script by forgetting the path argument, that just really fucked up my server. Ok, I'm the guy who copy paste things and misuse them, but guys, be wise and add checks / confirmations on scripts that delete / move stuffs like that...
    – Dulgan
    Commented Aug 1, 2018 at 8:32
  • 1
    Whoops! I am sorry to hear that. Hope you have a backup... I added a confirmation for future protection.
    – Bruno
    Commented Aug 2, 2018 at 10:34
  • 1
    Thanks @Bruno that's much better this way. My server is still running flawlessly, i commented the "flatten" part to just delete empty directories recursively from (and that was my error) root, until I saw an error that made me stop running the script.
    – Dulgan
    Commented Aug 2, 2018 at 14:05

You must log in to answer this question.

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