Take the 2-minute tour ×
Programming Puzzles & Code Golf Stack Exchange is a question and answer site for programming puzzle enthusiasts and code golfers. It's 100% free, no registration required.

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

  1. renames all files (including directories) in the current directory to versions with spaces removed or replaced by '_'
  2. on collision, you need to append a unique identifier (up to you)
  3. 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:

rm -r TestDir

    touch my_file
    touch my__file
    touch "my file"
    touch "my  file"
    touch " my_file  "

mkdir TestDir
cd TestDir


for dir in "Test Sub" Test_Sub "Te stSub" Te_stSub
    mkdir "$dir"
    cd "$dir"
    cd ..
share|improve this question
This is begging for a solution made without ascii chars. –  Dennis Jaheruddin Aug 6 '14 at 15:44
Now I want to learn Whitespace –  BrunoJ Aug 6 '14 at 18:03
@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. –  Nate Kerkhofs Aug 7 '14 at 6:40
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] –  Mark K Cowan Aug 7 '14 at 13:35
I hate underscores in filenames. Use dashes. –  Dr. Rebmu Aug 7 '14 at 17:07

14 Answers 14

up vote 4 down vote accepted

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}// /}
share|improve this answer
this does not rename my files at all! –  M.Herzkamp Aug 11 '14 at 11:56
@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. –  Gilles Aug 11 '14 at 16:15
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 –  M.Herzkamp Aug 12 '14 at 7:36

Bash 116 bytes, 16 spaces

find . -depth -exec bash -c 'B=${0##*/}
M="${0%/*}/${B// /_}"
while [ -e "$M" ]
do M=$M.
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.
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.
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)

share|improve this answer
IS THE EXTRA SPACE BEHIND find THERE TO MOCK ME??? ;-) –  M.Herzkamp Aug 6 '14 at 22:01
+1 for the first answer to work out of the box! –  M.Herzkamp Aug 6 '14 at 22:11
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 ;-) –  M.Herzkamp Aug 6 '14 at 22:21
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 Aug 7 '14 at 14:53
I guess I will be that guy, what is up with N variable? It is never defined... –  Steven Penny Aug 9 '14 at 5:04

Python 180 bytes

from    os  import*
def g(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)

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

share|improve this answer
I guess most of the other answers could improve their score by using tabs instead of spaces as well. –  kasperd Aug 7 '14 at 8:14
But your submission uses spaces doesn't it? (+1 for working code) –  M.Herzkamp Aug 7 '14 at 9:40
I don't know how to att tab characters in the answer... –  Emanuele Paolini Aug 7 '14 at 10:11
replaced with tabs :-) –  Emanuele Paolini Aug 7 '14 at 11:14
How ugly... Well, I guess I asked for it :( –  M.Herzkamp Aug 7 '14 at 11:33

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.

share|improve this answer
You can use -d instead of -depth and you don't need the quotes around {}. –  Dennis Aug 8 '14 at 19:21
@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. –  Etan Reisner Aug 8 '14 at 19:30
When collision happen on directory names, it will put one into another (and not strip spaces). Use -T option to mv to avoid this –  pqnet Aug 8 '14 at 22:53
This works, and I said in the challenge that the appendix is up to you. +1 –  M.Herzkamp Aug 12 '14 at 7:48

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('.');
share|improve this answer
I am not familiar with node.js. How would I run it? –  M.Herzkamp Aug 7 '14 at 12:22
You'll need the Node executable nodejs; save it in a file and run node file.js –  c.P.u1 Aug 7 '14 at 12:27
I get an exception TypeError: Object #<Object> has no method 'exists'. Guess where: it's in line 1! :D –  M.Herzkamp Aug 7 '14 at 12:45
I have tested it. Anyway, I replaced exists with its synchronous counterpart. Can you try now? –  c.P.u1 Aug 7 '14 at 12:53
I have only version 0.6.12 installed. That may be the problem. –  M.Herzkamp Aug 7 '14 at 13:26

Bash - 86 bytes

find    .   -d|while    IFS=""  read    f;do    t=${f##*/};mv   --b=t   -T  "$f"    "${f%/*}"/${t// /};done
share|improve this answer
Oops, will have a look –  Subbeh Aug 6 '14 at 13:04
Also, spaces are counted twice ;-) –  M.Herzkamp Aug 6 '14 at 13:07
what exactly do you mean with spaces are counted twice? –  Subbeh Aug 6 '14 at 13:14
You can save a lot of characters by abbreviating --backup to --b –  professorfish Aug 6 '14 at 13:17
Yes, now it works with my test set as well! +1 –  M.Herzkamp Aug 7 '14 at 12:47

Bash + Perl rename 64

(rename is the Perl script on Debian and derivatives, not the util-linux command.)

find . -depth -name "* *" -execdir rename 'y/ /_/' * \;
share|improve this answer
What happens if "my file.txt" and "my_file.txt" are both present? –  M.Herzkamp Aug 6 '14 at 12:15
Oh true.. Working on that soon –  german_guy Aug 6 '14 at 12:16
* 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. –  Gilles Aug 8 '14 at 21:28

Ruby 121

require 'find'

Find.find('.') do |file|
  if file.chomp.match(/ /)
    File.rename(file, file.gsub(/ /, '_'))
share|improve this answer
Welcome to Code Golf! The idea here in these code-golf challenges is to use the fewest number of characters. That means that you can definitely get rid of blank lines and tabs, and make variable names single-character, but people look for all sorts of creative ways to reduce character count. –  Not that Charles Aug 6 '14 at 20:23
I get an error, that the Directory is not empty:gam3.rb:5:in `rename': Directory not empty - ./Te stSub or ./Te_stSub (Errno::ENOTEMPTY) from gam3.rb:5 from /usr/lib/ruby/1.8/find.rb:39:in `find' from /usr/lib/ruby/1.8/find.rb:38:in `catch' from /usr/lib/ruby/1.8/find.rb:38:in `find' from gam3.rb:3 –  M.Herzkamp Aug 6 '14 at 21:04

Python, 187

165, plus 22 penalty points for the spaces.

from os import*
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

166, using Emanuele's \t trick:

Only a single space in this one!

from    os  import*
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
share|improve this answer
This works for me. +1 –  M.Herzkamp Aug 8 '14 at 9:08
remove the spaces at the beginning of the lines and use tabs - they are no spaces so count only once –  chill0r Aug 13 '14 at 15:13
@chill0r That's what the second version is; all the spaces are replaced with tabs except one (except SO still displays them as spaces). –  Henry Keiter Aug 13 '14 at 15:33

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...

share|improve this answer
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 Aug 11 '14 at 11:44
@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 –  mikeserv Aug 11 '14 at 12:08
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 Aug 11 '14 at 16:26
@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. –  mikeserv Aug 11 '14 at 17:50
@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. –  mikeserv Aug 12 '14 at 5:45

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.

share|improve this answer
Works! +1 I am going to delete my comments earlier to tidy this post. –  M.Herzkamp Aug 14 '14 at 11:42

Bash 4+ 111 bytes

shopt -s dotglob globstar
for f in **
n=${f// /}
while [[ $f != $n && -e $n ]]
do n+=1
mv "$f" $n
share|improve this answer
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. –  M.Herzkamp Aug 8 '14 at 8:59

Groovy, 139 characters

def c
def g=new File(f.parent,f.name.replaceAll('\\s',''))
!g.directory ?: g.eachFile(c)
new File('.').eachFile(c)

according to @edc65 comment

Groovy, handle collisions, 259 characters

def c
def g=new File(p,f.name.replaceAll('\\s',''))
def r=new File('.')
share|improve this answer
This does not handle collisions. –  edc65 Aug 13 '14 at 13:00
Make sure that files are renamed before their parent directories are, and that the spaces in parent directories are not replaced. –  M.Herzkamp Aug 14 '14 at 11:49
I'm sure it's ok –  login Aug 14 '14 at 15:36

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`
share|improve this answer
@M.Herzkamp Fixed. –  LinGeek Aug 6 '14 at 13:14
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. –  M.Herzkamp Aug 6 '14 at 13:34
The c replaces spaces with newlines. The IFS stops spaces being separators. The --reply is from old versions and is about to be fixed. –  LinGeek Aug 6 '14 at 13:49
Are you missing a second mv in line 5? And I think one echo in that line is wrong. –  M.Herzkamp Aug 6 '14 at 20:58
$(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 Aug 8 '14 at 21:31

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.