Flatten Directory but Preserve Directory Names in New Filename

Warning: I typed most of these commands directly in my browser. Caveat lector.

With zsh and zmv:

zmv -o -i -Qn '(**/)(*)(D)' '${1//\//-}$2'

Explanation: The pattern **/* matches all files in subdirectories of the current directory, recursively (it doesn't match files in the current directory, but these don't need to be renamed). The first two pairs of parentheses are groups that can be refered to as $1 and $2 in the replacement text. The final pair of parentheses adds the D glob qualifier so that dot files are not omitted. -o -i means to pass the -i option to mv so that you are prompted if an existing file would be overwritten.


With only POSIX tools:

find . -depth -exec sh -c '
    for source; do
      case $source in ./*/*)
        target="$(printf %sz "${source#./}" | tr / -)";
        mv -i -- "$source" "${target%z}";;
      esac
    done
' _ {} +

Explanation: the case statement omits the current directory and top-level subdirectories of the current directory. target contains the source file name ($0) with the leading ./ stripped and all slashes replaced by dashes, plus a final z. The final z is there in case the filename ends with a newline: otherwise the command substitution would strip it.

If your find doesn't support -exec … + (OpenBSD, I'm looking at you):

find . -depth -exec sh -c '
    case $0 in ./*/*)
      target="$(printf %sz "${0#./}" | tr / -)";
      mv -i -- "$0" "${target%z}";;
    esac
' {} \;

With bash (or ksh93), you don't need to call an external command to replace the slashes by dashes, you can use the ksh93 parameter expansion with string replacement construct ${VAR//STRING/REPLACEMENT}:

find . -depth -exec bash -c '
    for source; do
      case $source in ./*/*)
        source=${source#./}
        target="${source//\//-}";
        mv -i -- "$source" "$target";;
      esac
    done
' _ {} +

While there are good answers already here, I find this more intuitive, within bash:

find aaa -type f -exec sh -c 'new=$(echo "{}" | tr "/" "-" | tr " " "_"); mv "{}" "$new"' \;

Where aaa is your existing directory. The files it contains will be moved to the current directory (leaving only empty directories in aaa). The two calls to tr handle both directories and spaces (without logic for escaped slashes - an exercise for the reader).

I consider this more intuitive and relatively easy to tweak. I.e. I could change the find parameters if say I want to find only .png files. I can change the tr calls, or add more as needed. And I can add echo before mv if I want to visually check that it will do the right thing (then repeat without the extra echo only if things look right:

find aaa -name \*.png -exec sh -c 'new=$(echo "{}" | tr "/" "-" | tr " " "_"); echo mv "{}" "$new"' \;

Note also I'm using find aaa... and not find .... or find aaa/... as the latter two would leave funny artifacts in the final file name.


find . -mindepth 2 -type f -name '*' |
  perl -l000ne 'print $_;  s/\//-/g; s/^\.-/.\// and print' |
    xargs -0n2 mv 

Note: this will not work for filename which contain \n.
This, of course only moves type f files...
The only name clashes would be from files pre-existing in the pwd

Tested with this basic subset

rm -fr junk
rm -f  junk*hello*

mkdir -p  junk/junkier/junkiest
touch    'hello    hello'
touch    'junk/hello    hello'
touch    'junk/junkier/hello    hello'
touch    'junk/junkier/junkiest/hello    hello'

Resulting in

./hello    hello
./junk-hello    hello
./junk-junkier-hello    hello
./junk-junkier-junkiest-hello    hello

Tags:

Rename

Find