# Bulk rename of files containing spaces



## Alain De Vos (Aug 27, 2022)

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


----------



## rsronin (Aug 27, 2022)

Maybe you can use: sysutils/detox/.


----------



## facedebouc (Aug 27, 2022)

I like a lot sysutils/p5-File-Rename
Like this for example `$ rename 's/\s+/_/g' *`


----------



## zirias@ (Aug 27, 2022)

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!


----------



## Eric A. Borisch (Aug 27, 2022)

With bash:

`find . -type f | while read N; do U="${N// /_}"; [ "$U" != "$N"] && mv "$N" "$U"; done`

See “Pattern Substitution” in bash(1).


----------



## zirias@ (Aug 27, 2022)

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)


----------



## Erichans (Aug 27, 2022)

zirias@ said:


> [...] `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"



Eric A. Borisch said:


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


----------



## facedebouc (Aug 27, 2022)

`$ find /path_to_dir_to_rename/ | rename -d 's/\s+/_/g'`
Sorry doesn't work with subdirs


----------



## Alain De Vos (Aug 27, 2022)

I tried :

```
#!/usr/local/bin/zsh
for f in *; do
    (
        mv -v -- "$f" $(echo "$f" | tr " ,.()~-" '_'); 
    )
done
```
But it does not traverse subdirectories...


----------



## Alain De Vos (Aug 27, 2022)

zirias@ said:


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


In a first run i want to remove the spaces from the directories ?


----------



## facedebouc (Aug 27, 2022)

With 2 passes :

```
find . -type d | sort -r | rename -d 's/\s+/_/g'
find . -type f | rename 's/\s+/_/g'
```


----------



## Eric A. Borisch (Aug 27, 2022)

Alain De Vos said:


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


----------



## Alain De Vos (Aug 28, 2022)

rsronin said:


> Maybe you can use: sysutils/detox/.


Interesting, it just leaves ' i.e. single quotes in place in the filenames.


----------



## gpw928 (Aug 28, 2022)

Building on the work of Eric A. Borisch, I think that this will do it (with bash):
	
	



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


----------



## zirias@ (Aug 28, 2022)

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.


----------



## getopt (Aug 28, 2022)

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.


----------



## zirias@ (Aug 28, 2022)

Your typical "dry-run" with any shell command involving loops/find/...: replace the actual operation (here `mv`) with `echo`.


----------



## gpw928 (Aug 28, 2022)

My solution is to descend the tree, and operate on one level at a time (with bash):
	
	



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


----------



## zirias@ (Aug 28, 2022)

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.


----------



## _al (Aug 28, 2022)

Everything has been said, I'll just add a Perl solution 

```
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 (Aug 28, 2022)

zirias@ said:


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


----------



## _al (Aug 29, 2022)

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.


----------



## _al (Aug 29, 2022)

The solution from post 11 also works a little differently than mine. 
Here are the results on the test tree from the previous post.


----------



## gpw928 (Aug 29, 2022)

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


----------



## _al (Aug 29, 2022)

Alain De Vos,
Thank you for an interesting question, implying a non-trivial solution


----------

