How can I flatten a directory in the following format?

Before: ./aaa/bbb/ccc.png

After: ./aaa-bbb-ccc.png

  • PS: Please also account for file names with odd characters (e.g. spaces) Aug 16, 2012 at 16:54
  • 3
    You'll also need to specify how you want to handle the possible case where ./foo/bar/baz.png and ./foo-bar-baz.png both exist. I assume you don't want to replace the latter with the former?
    – jw013
    Aug 16, 2012 at 17:21
  • In my one case, it's irrelevant, but the answer should indeed account for that so others can rely on it! Thanks! Aug 16, 2012 at 19:13

6 Answers 6


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.

  • 1
    Thanks for this one liner - this worked well for me when I did it from outside of the directory (which is exactly what you said to do). When I tried to do from inside the directory (hoping to avoid the root directory name being added) all of the files "disappeared". I had turned them into hidden files in that same directory.
    – dmgig
    Aug 4, 2018 at 17:19
  • I like this one better and easier to understand. find with -exec makes things so much cleaner.
    – dina
    May 7, 2021 at 5:40

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}";;
' _ {} +

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}";;
' {} \;

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 ./*/*)
        mv -i -- "$source" "$target";;
' _ {} +
  • The for source examples miss the first presented file/dir... I've finally found out why you (normally) have used _ as $0 :) ... and thanks for the great answers. I learn so much from them.
    – Peter.O
    Aug 17, 2012 at 5:20
  • Note that the zmv example just does a dry run: it prints out the move commandsbut it doesn't execute them. To actually execute those commands, remove the n in -Qn.
    – Peter
    Jul 3, 2017 at 17:38
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

Here is an ls version.. comments welcome. It works for whacky filenames like this "j1ळ\n\001\n/j2/j3/j4/\nh  h\n", but I am really interested to know of any pitfalls. It is more of an exercise in "can the maligned ls actually do it (robustly)?"

ls -AbQR1p * |                        # paths in "quoted" \escaped format
    sed -n '/":$/,${/.[^/]$/p}' |     # skip files in pwd, blank and dir/ lines
    { bwd="$PWD"; while read -n1;     # save base dir strip leading "quote
                        read -r p; do # read path
        printf -vs "${p%\"*}"         # strip trailing quote"(:)
        [[ $p == *: ]] && {           # this is a directory
            cd   "$bwd/$s"            # change into new directory
            rnp="${s//\//-}"-         # relative name prefix
        } || mv "$s" "$bwd/$rnp$s"
      done; }

Just pipe the filename + path to tr command. tr <replace> <with>

for FILE in `find aaa -name "*.png"`
  NEWFILE=`echo $FILE | tr '/' '-'`
  cp -v $FILE $NEWFILE
  • 1
    This doesn't handle odd filenames safely. Spaces will break it (among other things).
    – jw013
    Aug 16, 2012 at 17:46
  • On first glance, this is a clean, readable solution but @jw013 is right, it won't handle spaces well. Would changing the cp line to cp -v "$FILE" "$NEWFILE" help? (Also, you should not assume only png files.) Aug 16, 2012 at 17:49
  • 2
    @NathanJ.Brauer Adding quotes to the cp line is insufficient. The entire approach of parsing find output with a for loop is flawed. The only safe ways to use find are with -exec, or -print0 if available (less portable than -exec). I'd suggest a more robust alternative but your question is under-specified at the moment.
    – jw013
    Aug 16, 2012 at 17:57

Safe for spaces in filenames:


/bin/find $PWD -type f | while read FILE
    #echo $FILE
    #echo $_new

    /bin/mv "$FILE" "$_new"

Ran mine with cp instead of mv for obvious reasons, but should produce the same.

  • 3
    type find -> find is /usr/bin/find on my machine. Using PATH as a variable name in a script is a terrible idea. Also, your script mangles file names with spaces or backslashes, because you misuse the read command (search this site for IFS read -r). Aug 16, 2012 at 18:04
  • find is hashed (/bin/find), relevant how? Different distros are different?
    – Tim
    Aug 16, 2012 at 18:08
  • Knew I should have stayed away from this, vulture bait.
    – Tim
    Aug 16, 2012 at 18:09
  • @Tim You can always delete the answer to get your rep back (and mine as well b/c it costs rep to downvote). Hard-coding paths into scripts seems to suggest that you are missing the point of the PATH environment variable, which is something every Unix system uses. If you write /bin/find then your script is actually broken on every system (Gilles, mine, and probably the OP's) that doesn't have /bin/find (e.g. b/c it is in /usr/bin/find). The whole point of the PATH environment variable is to make this a non-issue.
    – jw013
    Aug 16, 2012 at 18:17
  • By the way, $(pwd) is already available in a variable: $PWD. But you must not use an absolute path here: if your script is invoked from, say, /home/tim, it attempts to rename /home/tim/some/path to /home-tim-some-path. Aug 16, 2012 at 18:26

You must log in to answer this question.

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