Unix & Linux Stack Exchange is a question and answer site for users of Linux, FreeBSD and other Un*x-like operating systems. It's 100% free, no registration required.

Sign up
Here's how it works:
  1. Anybody can ask a question
  2. Anybody can answer
  3. The best answers are voted up and rise to the top

How can I flatten a directory in the following format?

Before: ./aaa/bbb/ccc.png

After: ./aaa-bbb-ccc.png

share|improve this question
PS: Please also account for file names with odd characters (e.g. spaces) – Nathan J. Brauer Aug 16 '12 at 16:54
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 '12 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! – Nathan J. Brauer Aug 16 '12 at 19:13

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";;
' _ {} +
share|improve this answer
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 '12 at 5:20
@Peter.O Fixed, thanks. – Gilles Aug 17 '12 at 7:04
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
share|improve this answer

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; }
share|improve this answer

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
share|improve this answer
This doesn't handle odd filenames safely. Spaces will break it (among other things). – jw013 Aug 16 '12 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.) – Nathan J. Brauer Aug 16 '12 at 17:49
@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 '12 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.

share|improve this answer
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). – Gilles Aug 16 '12 at 18:04
find is hashed (/bin/find), relevant how? Different distros are different? – Tim Aug 16 '12 at 18:08
Knew I should have stayed away from this, vulture bait. – Tim Aug 16 '12 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 '12 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. – Gilles Aug 16 '12 at 18:26

Your Answer


By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.