Bulk rename of files containing spaces

I want to bulk rename all files containing spaces in a directory and it's subdirectories to the same filename where the spaces are translated to underscores.
This makes move so much easier.
Can i use a command for this or a small script
 
Well, there are probably lots of nice tools, but this job is easily done with sh(1):
for i in *\ *; do n=$(echo $i | tr ' ' '_'); mv "$i" "$n"; done

edit: missed the "recursive" part, then this should work (given the directories don't have spaces themselves):
for i in **/*\ *; do n=$(echo $i | tr ' ' '_'); mv "$i" "$n"; done

edit2: version dealing correctly with spaces in directory names:
for i in **/*\ *; do n="$(dirname $i)/$(basename $i | tr ' ' '_')"; mv "$i" "$n"; done
Ok, that's probably not that simple/easy any more ;-)


edit3: no, the last version can't properly deal with spaces in the directories. See post #15 further down for a solution for that scenario!
 
  • With bash as well, the variant using for and a glob most likely performs better (or if you use find, give it a pattern as well)
  • This solution would also fail for subdirectories containing spaces in their names (just like the second code I posted above)
 
[...] for i in **/*\ *; do n="$(dirname $i)/$(basename $i | tr ' ' '_')"; mv "$i" "$n"; done
Ok, that's probably not that simple/easy any more ;-)
Consider the appropriate use of the options -i -n -v of mv(1) to prevent overwriting an existing file; for example when the following two files exist in the same directory: "this file" and "this_file"

With bash:

find . -type f | while read N; do U="${N// /_}"; [ "$U" != "$N"] && mv "$N" "$U"; done
I think the bash script guards against overwriting an existing file.
 
Last edited:
I tried :
Code:
#!/usr/local/bin/zsh
for f in *; do
    (
        mv -v -- "$f" $(echo "$f" | tr " ,.()~-" '_'); 
    )
done
But it does not traverse subdirectories...
 
Well, there are probably lots of nice tools, but this job is easily done with sh(1):
for i in *\ *; do n=$(echo $i | tr ' ' '_'); mv "$i" "$n"; done

edit: missed the "recursive" part, then this should work (given the directories don't have spaces themselves):
for i in **/*\ *; do n=$(echo $i | tr ' ' '_'); mv "$i" "$n"; done

edit2: version dealing correctly with spaces in directory names:
for i in **/*\ *; do n="$(dirname $i)/$(basename $i | tr ' ' '_')"; mv "$i" "$n"; done
Ok, that's probably not that simple/easy any more ;-)
In a first run i want to remove the spaces from the directories ?
 
In a first run i want to remove the spaces from the directories ?
Run my version above with -type d (instead of f) first to rename directories with spaces. Then run the initial version to rename files. Double check my options if you are concerned about overwriting a file that already exists, or add a check.

Edit: just realized this won’t work as described assuming you have directories at multiple depths getting renamed. Sorry.
 
Building on the work of Eric A. Borisch, I think that this will do it (with bash):
Code:
# fix all the leaf node files
D=$1
find $D -type f -regex '.*/[^/]* [^/]*$' | while read N; do U="${N// /_}"; [ "$U" != "$N"] && mv "$N" "$U"; done
 
# fix all the directories, sorting so longest paths are enumerated (and fixed) first
find $D -type d -regex '.* .*' | sort -r | while read N; do [ -d "$N" ] || continue; U="${N// /_}"; [ "$U" != "$N"] && mv "$N" "$U"; done

Edit: this won't work either. Back to the drawing board...
 
Ok, if you want to rename directories as well, we need a little help from find(1) to do it in POSIX sh(1):
find . -name \*\ \* -depth -exec sh -c 'n="`dirname \"$0\"`/`basename \"$0\" | tr \" \" _`"; mv "$0" "$n"' \{\} \;

Short explanation: Key is the -depth option, it modifies find(1)'s recursion to descent into directories first, which makes renaming files in the loop safe, even when renaming the directories as well because this happens last.

Hint: if you want to replace more characters (not only spaces), just add them to the first argument to tr(1) and the "find" pattern.
 
When command lines are getting too long they tend to be prone to errors and need a phase of time consuming testing.

Tools like sysutils/rename do have the option for dry runs while creating the regex expression. Running dry runs before doing bulk file operations should be considered.
 
Your typical "dry-run" with any shell command involving loops/find/...: replace the actual operation (here mv) with echo. ?‍♂️
 
My solution is to descend the tree, and operate on one level at a time (with bash):
Code:
D=${1:-`pwd`}
maxd=$(cd $D; find . -type d | awk -F"/" 'NF > max {max = NF} END {print max}')
level=1
while [ $level -lt $maxd ]
do
    find $D -maxdepth $level -regex '.* .*' | while read N
    do
     # U=$(echo "$N" | sed -e 's/ /_/g')    # Bourne shell
     U="${N// /_}"                # bash
     [ "$U" != "$N" ] && mv "$N" "$U"
    done
    level=$((level+1))
done
 
gpw928 this looks like it will work, but is pretty close to just developing a new tool ?
JFTR, -depth in find(1) conforms to POSIX.1, so should be fairly portable – and it avoids all the hassle, enabling a simple "single pass" solution.
 
Everything has been said, I'll just add a Perl solution ;)
Perl:
use Cwd 'abs_path';
use File::Find;
use File::Copy;
my @dirs=('.');
my @arr;
sub collect {
    while(@_){
        shift;
        chomp;
        my $fn = abs_path($_);
        push @arr, $fn;
    }
}
find sub {collect($File::Find::name)} , @dirs;
my @a1=reverse sort @arr;
my @a2=@a1;
for (my $i=0; $i<@a1; $i++) {
    if($a2[$i] =~ m{(.+)(/)(.+)}){
        my $c1=$1;
        my $c3=$3; 
        for($c3){s/\s/_/g} 
        $a2[$i]=$c1.'/'.$c3;
    }
    unless($a1[$i] eq $a2[$i]){
        print $a1[$i],'-->',$a2[$i];
        unless( -e $a2[$i]){
            move($a1[$i],$a2[$i]);
            print ": ok\n";
        } else {
            print ": destination exists, source will not be renamed\n";
        }
    }
}
If the target file or directory exists, it will not be replaced.
Files/sub-directories in this existing directory will be renamed in the same way.
Edit: Minor corrections for better readability, diagnostic messages added
 
gpw928 this looks like it will work, but is pretty close to just developing a new tool ?
Both of our solutions work.

Your solution is quite elegant in the sense that it does it in one pass. It also has no edge conditions (e.g. depth calculation) to worry about.

My solution does benefit from better readability.

It's a surprisingly difficult problem. On balance, I like your solution better...
 
Solutions from posts 15 and 18 work a little differently than mine (post15_results.zip, post18_results.zip).
Could you try it on this test subtree (test_dirs.zip) ?
My solution (post 20) is far from perfect, but if I understand the problem correctly, I think it should work like mine (post20_results.zip) . :-/ Correct me if I am wrong.
 

Attachments

My method parses filenames with the shell, and trailing spaces are lost. Not sure that was an issue because having file names with trailing spaces is really asking for trouble!

However, the method used by zirias@ handles trailing spaces correctly.

Your method also handles trailing spaces correctly. In addition it takes action when the target of a rename already exists (very sensible, but not part of the original spec).
 
Back
Top