text:
I HATE spaces in file names
It is simple. I cannot stand when people use spaces when naming files. It sometimes wrecks console commands and makes the output of ls ugly.
The challenge is to write a program (only ascii characters) which

renames all files (including directories) in the current directory to versions with spaces removed or replaced by '_'
on collision, you need to append a unique identifier (up to you)
descends recursively into all subdirectories

You can assume UNIX-style path names. Who would need this program on a Windows machine anyways?
This is code golf, the shortest program wins (#ascii characters). Since I hate spaces so much, each space has to be counted twice.
Please provide your language, score, program and a short description of how to run it.
The program must compile and execute with reasonable effort on my linux machine.
EDIT: As Etan requested a file structure for testing, here is the script I currently use to create a suitable file tree:
#!/bin/bash
rm -r TestDir

touchfiles()
{
    touch my_file
    touch my__file
    touch "my file"
    touch "my  file"
    touch " my_file  "
}

mkdir TestDir
cd TestDir

touchfiles

for dir in "Test Sub" Test_Sub "Te stSub" Te_stSub
do
    mkdir "$dir"
    cd "$dir"
    touchfiles
    cd ..
done

renames all files in the current directory to versions with spaces removed

I will edit the challenge to allow for replacements.

@ДамянСтанчев I know, but the first submission used a replacement and the OP didn't complain. ;)

How to replace one char with another in all filenames of the current directories?

This is begging for a solution made without ascii chars.

Well, are you running TeX on a Windows machine? That will leave you viscerally hating that space you left in the name of your images directory.

Now I want to learn Whitespace

Is it okay to just replace all spaces in filenames on the system with random garbage from /dev/urandom?

You can remove spaces or replace them with '_'

@Dennis Edited the challenge to fill that loophole

Are "Tabs" (\t) considered spaces for scoring?

In case of name collision, should "unique identifier" be appended to the file "name", and the file "extension" left alone? Like: index 1.html -to- index1-02.html, not index1.html-02.

It might be wise to add an extended example. 'Collisions' get confusing with a deep tree where multiple subdirs have spaces.

@BrunoJ doing this in Whitespace would first require you to develop a file access system in WS. I think that would be more challenging than the actual challenge.

"Who would need this program on a Windows machine anyways?" - people calling "Program files" content in console ;) Its same issue as on Unix, and Windows for some time accepts paths with / as well.

@Kevin Append the identifier to the extension. I don't hate tabs as much, so they only count once.

@PTwr "Windows for some time accepts paths with / as well" — since always. And MS-DOS too (since directories, to be accurate).

@Athari thanks for clarifying, I was not sure if non-NT supported it.

Waiting for someone to post a C/C++ solution so I can steal it, compile, post in hex as x86 machine code with ZERO spaces! [or maybe base64]

I hate underscores in filenames.  Use dashes.

Is there a corpus of files to test solutions against?

@Dr. Rebmu: Why?

@Etan: included my test tree creator script

When collisions are encountered, do we have to append to the filename that had spaces or can we append to the original file as well?

@MarkKCowan you don't need to compile and use hex for that: all spaces outside strings can be substituted with newlines, and all spaces inside strings can be substituted with the corresponding ASCII code (32)

"I cannot stand when people use spaces when naming files. It sometimes wrecks console commands." ... man I hate this nail in my toolbox, it destroys my hammer!

You should include some characters that cause problems in badly-written shell scripts in your test harness: backslash, newline, tabs, *, …

@M.Herzkamp Don't have to hit shift to type them, considered a word separator in RegEx/Unicode, doesn't sink down and disappear into a bounding box if the filename winds up in a box, less awkward typographically if you ever have to communicate about a filename as a URL in print, there are probably other reasons...

@pqnet Compiled-to-hex asm would use less chars when posted here :)

Please include non English Characters as a test case.

It's tempting to write an answer in batch.. ;)

@MarkKCowan No need to do that. Just directly write in x86 machine code - it's a lot shorter that way.

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

this does not rename my files at all!

@M.Herzkamp Oh, right, zmv bombs out before mv has a chance to sort out collisions. Ok, I'm doing this manually. Turns out to be exactly the same length if I skip dot files and even saves a character if I don't.

Now it's working. Btw: I included the space penalty at a time where I really had a grudge against spaces ;) Ironically, I did not exclude spaces when I posted the challenge :P

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)

IS THE EXTRA SPACE BEHIND find THERE TO MOCK ME??? ;-)

@M.Herzkamp I though it was a copy and paste error, but it is actually there. Guess I gained 2 more points. Also, -depth in GNU can be replaced by -d, though it complains that it is deprecated. I don't know about the rules of golf, can I do that?

As long as it works, I allow it. Should the deprecation become removal in a future version though, I might have to come back to this answer and downvote it for not being correct ;-)

removed the extra space and added the non-posix version

Wow, I like it. Nice way to work around the problems, +1

@german_guy you mean about the trick of adding dots to the name until there is a name which is not present? In a previous version I used mktemp to generate a non-existing file with random numbers at the end, which I liked more, but I switched to this because it is a bit shorter

This won't work properly if any of the filenames contains a double quote. To fix this, you can use bash -c 'B=${0##*/}...' {} \; instead, which is actually shorter.

@Dennis makes sense. Now that I have a better look at it, it doesn't work if the file name starts with one or two dashes. I'll try to fix both issues then edit again

Are you sure? If find is called without a path, {} will always begin with ./.

@Dennis oh right it makes sense, no need to use --

I guess I will be that guy, what is up with N variable? It is never defined...

@StevenPenny error with edit, used to be $N but I removed the variable to gain more space. Should be $0 now. Thank you for noticing, I fixed it.

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 :-)

I guess most of the other answers could improve their score by using tabs instead of spaces as well.

But your submission uses spaces doesn't it? (+1 for working code)

I don't know how to att tab characters in the answer...

You can copy-paste them in, like I just did. The Markdown parser, alas, converts them to spaces, but you can always get the original code with tabs by clicking "edit".

There are still more than two spaces (e.g. in for statement)

replaced with tabs :-)

How ugly... Well, I guess I asked for it :(

You can remove all spaces by adding ,s=chr(32) at the top and replacing all instances of ' ' with s. That only increases the length to 188 bytes.

@Pluto: you gain 2 point and loose 8... so it is not convenient.

If the order of collided file suffixes does not need to give precedent to the pre-existing file then the following works for me:
bash/find/mv 84 bytes, 16 spaces
find -depth -execdir bash -c '[ "${0//[^ ]}" ] && mv -{T,--b=t} "$0" "${0// }"' {} \;

bash/find/mv 82 bytes, 14 spaces
find -depth -execdir bash -c '[ "${0//[^ ]}" ]&&mv -{T,-b=t} "$0" "${0// }"' {} \;

Cuddled && to save two space bytes.
bash/find/mv 60 bytes, 11 spaces
find -d -execdir bash -c 'mv -{T,-b=t} "$0" "${0// }"' {} \;

Drops error protection so it gets errors from mv on files which have no spaces to start with.
Edit: Dropped the quotes from {} as reminded by Dennis. Also allowed find to scream about portability and deprecation in the shortest version where mv is already screaming about moving a file on top of itself.
Edit 2: Added -T to mv command to avoid nesting directories instead of renaming as pointed out by pqnet. Used brace expansion at cost of one character over just using one space.

You can use -d instead of -depth and you don't need the quotes around {}.

@Dennis Yeah. I saw the -d conversation on pqnet's answer but figured since I was silencing the mv screaming I'd avoid the find screaming. Though I should probably shorten it for the screaming one. And yeah, I always quote {} for some reason even though I know you don't have to in this case. Force of habit I guess.

When collision happen on directory names, it will put one into another (and not strip spaces). Use -T option to mv to avoid this

This works, and I said in the challenge that the appendix is up to you. +1

NodeJS – 209 bytes, 3 Whitespaces
s=require('fs');function a(d){s.readdirSync(d).forEach(function(f){f=d+'/'+f;i=0;r=f;if(/ /.test(f)){r=f.replace(' ','');while(s.existsSync(r))r+=i++;s.renameSync(f,r)}s.statSync(r).isDirectory()&&a(r)})}a('.');

I am not familiar with node.js. How would I run it?

You'll need the Node executable nodejs; save it in a file and run node file.js

I get an exception TypeError: Object #<Object> has no method 'exists'. Guess where: it's in line 1! :D

I have tested it. Anyway, I replaced exists with its synchronous counterpart. Can you try now?

Still the same exception. Somehow s is not initialised properly. Is there any package I need to run it? Also, I believe you want to replace spaces with numbers. However, the challenge states to either remove them or to replace them with an underscore.

Well, I'm running Node v0.11.12 on OS X and it works. As to the second part, I have edited my post to reflect that. BTW, If s weren't initialised, the first call to s.readdirSync would've thrown an exception. Also s is assigned only once.

I have only version 0.6.12 installed. That may be the problem.

You can probably remove the Synchs again, and the i=0; seems superfluous now.

Just now compiled v0.10.30, and it works like a charm. +1

This solution does not handle multiple spaces within a file/directory name. Here is a version which fixes that issue and is optimized to 190 bytes with no whitespaces: http://pastie.org/9456328

POSIX sh + GNU find + GNU mv 67 ASCII bytes + one (literal) space
find    -d  -exec   sh  -cf 'IFS=\ ;IFS=_   set $0;mv   --b=t   "$0"    "$*"'   {}  \;

I don't know if it fits, but with this any sequence of spaces is elided to a single _ - I like it anyway. Actually any sequence but leading/trailing spaces that is - those are automatically truncated (which is also, I think, a beneficial behavior). Thanks to Gilles for pointing this out.
This just uses the internal field separator to separate fields.
It's fairly... chatty...
...oh man. I knew the tab thing was cheap, but I thought it was at least clever. Now I'm just late to the party...

This works on my test set as you intended, but not as the challenge requires. I like it, though, because I will probably learn something new. I guess I will have to read up on this IFS magic thingy...

@M.Herzkamp - ifs behaves differently depending on whether it is set to whitespace or not. Most people hate it because they do not understand its two primary qualities - that it only operates on expansions ($expand not (ex pand)) and the ifsws thing just mentioned. Look here

This doesn't rename files inside directories whose names contain spaces. A fix would be to replace -exec with -execdir. Ånother quirk of IFS that you aren't mentioning is that trailing spaces are deleted. Note that as others have noticed you need the -T option to mv as well, for when the target of an mv call is an existing directory.

@Gilles - my preference would be to use sh -c 'mkdir -p ../newtree/"$0"; ln "$0"/* ../newtree/$0 {} \; and other globs on a find  -type d command to create a mirrored tree of hardlinks and then to operate on those, but I'm second-guessing writing a code-golf at all for a move operation. Good point about the leading/trailing spaces, though I think that is also a behavior I would prefer.

@Gilles - but by the way, it is not a quirk - it is an intended and standards-controlled behavior. The Field-Splitting section is among the very few in the shell spec that does not contain the words unspecified or implementation-defined. There are no such guarantees with zsh's builtin function zmv for instance.

On further reflection, my earlier judgement seems to be unjustified. The challenge says that you can either remove spaces or replace them with an underscore. However, it does not mention that it has to be done consistently. If you chose to replace some spaces and remove others, it seems to be a valid solution. +1

@M.Herzkamp perhaps valid, but still crazy. Dont golf your fs - there are too many sandtraps.

Bash + Perl rename 64
(rename is the Perl script on Debian and derivatives, not the util-linux command.)
find . -depth -name "* *" -execdir rename 'y/ /_/' * \;

What happens if "my file.txt" and "my_file.txt" are both present?

Oh true.. Working on that soon

* should be {}, as it stands this only renames files whose name appears in the current directory. This doesn't append a suffix in case of collision. You could save quite a bit by omitting -name "* *" since rename silently ignores files whose name is not transformed.

Bash - 86 bytes
find    .   -d|while    IFS=""  read    f;do    t=${f##*/};mv   --b=t   -T  "$f"    "${f%/*}"/${t// /};done

Oops, will have a look

Also, spaces are counted twice ;-)

what exactly do you mean with spaces are counted twice?

@Subbeh a space counts as two bytes rather than one

You can save a lot of characters by abbreviating --backup to --b

if there is directory collision for some reason all files are put into the same directory. Also spaces are stripped out instead of being replaced by underscores (i guess you should use ${f// /_} )

It is ok to remove them. As long as the possible collisions are handled properly.

You get the following kind of error repeatedly: mv: cannot stat \./Test_Sub/ my_file': No such file or directory` Also, you have the problem, that you delete the spaces of parent directories in path names.

I changed the code to work better with parent directories. I don't see any problems with the testset I'm using now.

Your solution somehow has problems with files ending in spaces. I guess the read trims them?

Alright, seems like it's fixed now -_-

Yes, now it works with my test set as well! +1

while IFS="" read is missing -r after read to cope with backslashes. OTOH you can omit the "" after IFS=. See How to loop over the lines of a file? In the mv command, "${f%/*}"/${t// /} should be "${f%/*}/${t// }" (quote properly, and save 1 character).

@Etan: What exactly does not seem to work? I don't see any problems with the result.

You could use newlines intead of ;, will make your code more readable without increasing the number of bytes

Sorry, this does appear to work. I had a mv alias getting in the way it seems. But I guess that's a semi-valid caveat.

PHP, 147 145 bytes, 2 1 spaces -> 146
function    s(){foreach(glob("*")as$n){is_dir($n)&&chdir($n)&s()|chdir("..");if($n<$r=strtr($n," ",_)){while(file_exists($r))$r.=_;rename($n,$r);}}}

recursive function. Run with s(".");
Loop through glob results for given path:

if directory, recurse
replace spaces with underscore
if strings differ

while new filename is taken, append underscore
rename file/directory

php will rename the files on the server... Now I wonder how to change the file names of a client whenever they visit your site :D

POSIX(Tested on zsh) + basic Linux commands 151
export IFS='
'
for f in $(ls -R1);do export n=$(echo $f|tr ' ' '_');yes n|mv $f $n || yes n|mv $f `echo $n;echo $f|md5sum`
done

@M.Herzkamp Fixed.

Several things: what is the function of export IFS and the c in ls -cR? And what version of mv do you need for --reply option? (I have 8.13, and it does not recognise the option). Also to get a better score, you should abbreviate your variable names.

The c replaces spaces with newlines. The IFS stops spaces being separators. The --reply is from old versions and is about to be fixed.

@M.Herzkamp Fixed.

Are you missing a second mv in line 5? And I think one echo in that line is wrong.

$(ls -CR) is completely bogus. The -c option is useless, and -R gets you files without their directory, which is pointless. Your architecture fundamentally won't handle file names containing newlines. You need set -f or else file names containing wildcards will explode. export is useless. I can vaguely see what you're trying to do to uniquify files, but the piping is wrong.

@Gilles This works, because of the file structure in the challenge

The challenge is not limited to the tests given in the question, they're just that: tests. But your code (neither the original nor the updated) doesn't even work on these test cases (neither in bash nor in zsh)!

Ruby 194
require'find'
require'fileutils'
Find.find(?.).sort{|a,b| b.length<=>a.length}.each {|f|
if f.match(/ /)
o=f.tr(' ',?_)
begin
raise if File.exist? o
FileUtils.mv f,o
rescue
o+=?_
retry
end
end}

Python, 187
165, plus 22 penalty points for the spaces.
from os import*
u='_';j=path.join
for t,d,f in walk('.',0):
 for z in f+d:
  n=z.replace(' ',u)
  if n!=z:
   while path.exists(j(t,n)):n+=u
   rename(j(t,z),j(t,n))

166, using Emanuele's \t trick:
Only a single space in this one!
from    os  import*
u='_';j=path.join
for t,d,f   in  walk('.',0):
    for z   in  f+d:
        n=z.replace(' ',u)
        if  n!=z:
            while   path.exists(j(t,n)):n+=u
            rename(j(t,z),j(t,n))

This works for me. +1

remove the spaces at the beginning of the lines and use tabs - they are no spaces so count only once

@chill0r That's what the second version is; all the spaces are replaced with tabs except one (except SO still displays them as spaces).

LiveScript - 166
(Replace spaces with tabs.)
(a=->(s=require \fs)readdirSync(it)map (f)->f=it+'/'+f;r=f.replace /\s/g,i='';(while f!=r&&s.existsSync r=>r+=i++);s.statSync(f)isDirectory(s.renameSync f,r)&&a r) \.

Based on nderscore's optimized version of c.P.u1's answer.

Works! +1 I am going to delete my comments earlier to tidy this post.

Bash 4+ 111 bytes
shopt -s dotglob globstar
for f in **
do
n=${f// /}
while [[ $f != $n && -e $n ]]
do n+=1
done
mv "$f" $n
done

Same problems as several other entries: You replace spaces in parent directories and mv cannot find them. Also you must change direction of traversion, otherwise you rename directories and mv cannot find the files inside.

Groovy, 139 characters
def c
c={
f->
def g=new File(f.parent,f.name.replaceAll('\\s',''))
f.renameTo(g)
!g.directory ?: g.eachFile(c)
}
new File('.').eachFile(c)

according to @edc65 comment
Groovy, handle collisions, 259 characters
def c
c={
p,l,f->
def g=new File(p,f.name.replaceAll('\\s',''))
f==g?:
(g.exists()?f.renameTo(g.toString()+l.indexOf(f.name)):f.renameTo(g))
!g.directory?:g.eachFile(c.curry(g,g.list().toList()))
}
def r=new File('.')
r.eachFile(c.curry(r,r.list().toList()))

This does not handle collisions.

Make sure that files are renamed before their parent directories are, and that the spaces in parent directories are not replaced.

I'm sure it's ok

authors: Дамян Станчев