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