64
\$\begingroup\$

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:

#!/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
\$\endgroup\$
32
  • 24
    \$\begingroup\$ This is begging for a solution made without ascii chars. \$\endgroup\$ Aug 6, 2014 at 15:44
  • 51
    \$\begingroup\$ Now I want to learn Whitespace \$\endgroup\$
    – BrunoJ
    Aug 6, 2014 at 18:03
  • 10
    \$\begingroup\$ @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. \$\endgroup\$
    – Nzall
    Aug 7, 2014 at 6:40
  • 7
    \$\begingroup\$ 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] \$\endgroup\$ Aug 7, 2014 at 13:35
  • 11
    \$\begingroup\$ I hate underscores in filenames. Use dashes. \$\endgroup\$ Aug 7, 2014 at 17:07

15 Answers 15

13
\$\begingroup\$

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)

\$\endgroup\$
12
  • 11
    \$\begingroup\$ IS THE EXTRA SPACE BEHIND find THERE TO MOCK ME??? ;-) \$\endgroup\$
    – M.Herzkamp
    Aug 6, 2014 at 22:01
  • \$\begingroup\$ @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? \$\endgroup\$
    – pqnet
    Aug 6, 2014 at 22:16
  • 2
    \$\begingroup\$ 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 ;-) \$\endgroup\$
    – M.Herzkamp
    Aug 6, 2014 at 22:21
  • 2
    \$\begingroup\$ 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. \$\endgroup\$
    – Dennis
    Aug 7, 2014 at 14:53
  • 3
    \$\begingroup\$ I guess I will be that guy, what is up with N variable? It is never defined... \$\endgroup\$
    – Zombo
    Aug 9, 2014 at 5:04
10
\$\begingroup\$

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
\$\endgroup\$
3
  • 1
    \$\begingroup\$ this does not rename my files at all! \$\endgroup\$
    – M.Herzkamp
    Aug 11, 2014 at 11:56
  • \$\begingroup\$ @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. \$\endgroup\$ Aug 11, 2014 at 16:15
  • 1
    \$\begingroup\$ 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 \$\endgroup\$
    – M.Herzkamp
    Aug 12, 2014 at 7:36
10
\$\begingroup\$

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

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

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.

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

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

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

\$\endgroup\$
7
  • \$\begingroup\$ 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... \$\endgroup\$
    – M.Herzkamp
    Aug 11, 2014 at 11:44
  • 1
    \$\begingroup\$ @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 \$\endgroup\$
    – mikeserv
    Aug 11, 2014 at 12:08
  • \$\begingroup\$ 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. \$\endgroup\$ Aug 11, 2014 at 16:26
  • \$\begingroup\$ @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. \$\endgroup\$
    – mikeserv
    Aug 11, 2014 at 17:50
  • \$\begingroup\$ @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. \$\endgroup\$
    – mikeserv
    Aug 12, 2014 at 5:45
2
\$\begingroup\$

Bash - 86 bytes

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

Bash + Perl rename 64

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

find . -depth -name "* *" -execdir rename 'y/ /_/' * \;
\$\endgroup\$
3
  • 11
    \$\begingroup\$ What happens if "my file.txt" and "my_file.txt" are both present? \$\endgroup\$
    – M.Herzkamp
    Aug 6, 2014 at 12:15
  • 1
    \$\begingroup\$ Oh true.. Working on that soon \$\endgroup\$
    – german_guy
    Aug 6, 2014 at 12:16
  • 1
    \$\begingroup\$ * 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. \$\endgroup\$ Aug 8, 2014 at 21:28
2
\$\begingroup\$

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
\$\endgroup\$
1
  • \$\begingroup\$ 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 \$\endgroup\$
    – M.Herzkamp
    Feb 6, 2017 at 11:22
1
\$\begingroup\$

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))
\$\endgroup\$
3
  • \$\begingroup\$ This works for me. +1 \$\endgroup\$
    – M.Herzkamp
    Aug 8, 2014 at 9:08
  • \$\begingroup\$ remove the spaces at the beginning of the lines and use tabs - they are no spaces so count only once \$\endgroup\$
    – chill0r
    Aug 13, 2014 at 15:13
  • \$\begingroup\$ @chill0r That's what the second version is; all the spaces are replaced with tabs except one (except SO still displays them as spaces). \$\endgroup\$ Aug 13, 2014 at 15:33
1
\$\begingroup\$

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.

\$\endgroup\$
1
  • \$\begingroup\$ Works! +1 I am going to delete my comments earlier to tidy this post. \$\endgroup\$
    – M.Herzkamp
    Aug 14, 2014 at 11:42
1
\$\begingroup\$

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

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}
\$\endgroup\$
0
0
\$\begingroup\$

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
\$\endgroup\$
1
  • 1
    \$\begingroup\$ 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. \$\endgroup\$
    – M.Herzkamp
    Aug 8, 2014 at 8:59
0
\$\begingroup\$

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()))
\$\endgroup\$
3
  • 1
    \$\begingroup\$ This does not handle collisions. \$\endgroup\$
    – edc65
    Aug 13, 2014 at 13:00
  • \$\begingroup\$ Make sure that files are renamed before their parent directories are, and that the spaces in parent directories are not replaced. \$\endgroup\$
    – M.Herzkamp
    Aug 14, 2014 at 11:49
  • \$\begingroup\$ I'm sure it's ok \$\endgroup\$
    – login
    Aug 14, 2014 at 15:36

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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