Flattening directory hierarchy preserving directory names in new directory name

Something like this maybe?

#!/bin/sh

for topdir in */; do
    topdir_name=$( basename "$topdir" )

    for subdir in "$topdir"/*/; do
        subdir_name=$( basename "$subdir" )

        newdir="$topdir_name - $subdir_name"
        if mkdir "$newdir"; then
            mv "$subdir"/* "$newdir"
            rmdir "$subdir"
        fi
    done

    rmdir "$topdir"
done

This goes through all the top-level directories in the current directory (the band names). For each such directory, it goes through its subdirectories (the album names). For each pair of band name and album name, a new directory is created and the files from the subdirectory are moved to it. The album subdirectories are removed when they have been processed, as are the original band top-level directories.

The rmdir calls will fail if any directory contains hidden filenames or if any of the new directories failed to be created.

This is totally untested code. Run it on a backed-up copy of your files.


Zsh

Untested:

zmv -Q '(*)/(*)(/)' '$1 - $2'
rmdir -- *(/^F)

The second line removes all empty directories, even those that didn't have a file before. It's possible to work around this with a custom mv wrapper that records what directories it moves things from.

Note that this traverses symbolic links to directories in the current directory.

Linux rename utility

Untested.

rename / ' - ' */*/
rmdir -- */ 2>/dev/null

Note that this traverses symbolic links to directories in the current directory and in its subdirectories. The second line removes all empty directories, even those that didn't have a file before.

Perl rename script

Untested.

prename 's~/~ - ~' */*/
rmdir -- */ 2>/dev/null

Note that this traverses symbolic links to directories in the current directory and in its subdirectories. The second line removes all empty directories, even those that didn't have a file before.

Here's a more complex approach that only removes directories that it renamed something from. Again, untested.

prename 's~([^/]+)/~$1 - ~ and ++$d{$1}; END {map {rmdir} keys %d}' */*/

Strategically, rather than moving files, have you considered leaving the current structure in place and creating links for the new structure you want?

Tactically, a pattern that'll do the job goes like this:

find . -mindepth 2 -maxdepth 2 -type d -print0 | xargs -0n1 bash -c \
  'b=$(basename "$(dirname "$1")"); a=$(basename "$1"); echo ln -s "$1" "$b-$a"' {}
  • find locates all directories exactly two levels deep from the current working directory, which should be the directory containing the bands: thus two levels deep are the album names underneath the band names.1
  • xargs consumes each path containing an album and calls out to the inline bash script.
  • bash -c '...' takes the album path as its first argument, breaking that path into two parts: the band ($b) and the album ($a). Finally, the script reassembles the names into the desired format and links the new directory name to the original directory.

Note that, in this example, the links would be created in the same directory from which you start, which also happens to be where the band names are.

You could - and should - tweak the ln strategy above to match your intent. mv with the right paths if you physically want to rearrange, or ln if you're wanting to create a convenience "view" over the media. The important parts inside the bash script are:

  • $b the band name. Always quote it: "$b".
  • $a the album name. Always quote it: "$a".
  • $1 the physical path to the album directory. Always quote it: "$1".

1 I believe m{ax,in}depth are supported by GNU and some BSD find, but not POSIX: in which case, rely on */* or similar shell gymnastics.