I HATE spaces in file names

Bash 116 bytes, 16 spaces

find . -depth -exec bash -c 'B=${0##*/}
M="${0%/*}/${B// /_}"
while [ -e "$M" ]
do M=$M.
done
mv "$0" "$M"' {} \;

I didn't suppress errors to gain a couple more bytes. This will not have any collisions.

If non-posix GNU find can be expected, this can be shortened further:

Bash 110 bytes, 15 spaces

find -d -exec bash -c 'B=${0##*/}
M="${0%/*}/${B// /_}"
while [ -e "$M" ]
do M=$M.
done
mv "$0" "$M"' {} \;

Removing spaces instead of replacing them uses two less bytes:

Bash 108 bytes, 15 spaces

find -d -exec bash -c 'B=${0##*/}
M="${0%/*}/${B// }"
while [ -e "$M" ]
do M=$M.
done
mv "$0" "$M"' {} \;

Note: if tabs can be used instead of spaces, only 1 space is needed (the one in the match rule for substitution at line 2).

Thanks to Dennis for finding bug on double quote (and providing solution)


Zsh + GNU coreutils — 48 bytes (1 space)

for x   (**/*(Dod))mv   -T  --b=t   $x  $x:h/${${x:t}// }

It's weird that you hate (ASCII) spaces but are fine with tabs and newlines, but I guess it takes all kinds.

zmv solves a lot of file renaming problems concisely (and only slightly obscurely). However, it insists on the targets being unique; while you can easily add unique suffixes, adding a suffix only if it would be needed pretty much requires re-doing all the work. So instead I loop manually and rely on GNU mv to append a unique identifier in case of collision (--backup option, plus --no-target-directory in case a target is an existing directory, as otherwise mv would move the source inside that directory).

(od) is a glob qualifier to sort the output with directories appearing after their content (like find's -depth). D includes dot files in the glob. :h and :t are history modifiers similar to dirname and basename.

mv complains that it's called to rename files to themselves, because the glob includes file names without spaces. C'est la vie.

Ungolfed version:

for x in **/*\ *(Dod); do
  mv --no-target-directory --backup=numbered $x ${x:h}/${${x:t}// /}
done

Python 180 bytes

from    os  import*
t,c,h='.',chdir,path
def g(p):
    c(p)
    for x   in  listdir(t):
        if h.isdir(x):g(x)
        n=x.replace(' ','')
        while h.exists(n):n+=t
        if' 'in x:rename(x,n)
    c(t*2)
g(t)

only 2 spaces if you use tab for indentation :-)