362

Can anyone recommend a safe solution to recursively replace spaces with underscores in file and directory names starting from a given root directory? For example:

$ tree
.
|-- a dir
|   `-- file with spaces.txt
`-- b dir
    |-- another file with spaces.txt
    `-- yet another file with spaces.pdf

becomes:

$ tree
.
|-- a_dir
|   `-- file_with_spaces.txt
`-- b_dir
    |-- another_file_with_spaces.txt
    `-- yet_another_file_with_spaces.pdf
2
  • 10
    What do you want to happen if there is a file called foo bar and another file called foo_bar in the same directory? Commented Apr 25, 2010 at 18:56
  • Good question. I wouldn't want to overwrite existing files or lose any data. It should leave it unchanged.. ideally printing a warning but that's probably asking too much. Commented Apr 25, 2010 at 18:59

23 Answers 23

518

I use:

for f in *\ *; do mv "$f" "${f// /_}"; done

Though it's not recursive, it's quite fast and simple. I'm sure someone here could update it to be recursive.

The ${f// /_} part utilizes bash's parameter expansion mechanism to replace a pattern within a parameter with supplied string. The relevant syntax is ${parameter/pattern/string}. See: https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html or http://wiki.bash-hackers.org/syntax/pe .

Sign up to request clarification or add additional context in comments.

11 Comments

Simple and work in mac. (mac doesnt have rename, and its too hard to install this with brew..)
awesome answer. i used for d in *\ *; do mv "$d" "${d// /}"; done non under score.
For reference, this can easily become recursive in bash for using shopt -s globstar and for f in **/*\ *; do .... The globstar option is internal to bash, whereas the rename command is a common Linux tool and not part of bash.
${f// /_} is a Bash variable expansion for search and replace. - The f is the variable from the for loop for each file that contains a space. - The first // means "replace all" (don't stop at first occurrence). - Then the ` /_` means "replace space with underscore"
based on your solution, I made it recursive: while read line ; do mv "$line" "${line// /}" ; done < <(find /path/ -iname "* *")
|
397

Use rename (aka prename) which is a Perl script which may be on your system already. Do it in two steps:

find . -name "* *" -type d | rename 's/ /_/g'    # do the directories first
find . -name "* *" -type f | rename 's/ /_/g'

Based on JĂĽrgen's answer and able to handle multiple layers of files and directories in a single bound using the "Revision 1.5 1998/12/18 16:16:31 rmb1" version of /usr/bin/rename (a Perl script):

find . -depth -name "* *" -execdir rename 's/ /_/g' "{}" \;

21 Comments

No need for two steps: Use Depth-first search: find dir -depth
Oh, I've just read the rename manpage (I didn't know the tool) and I think you can optimize your code by changing s/ /_/g to y/ /_/ ;-)
If you're running this on OS X, you'll need to brew install rename
This doesn't work on Centos 7, as the rename command is completely different (it's a binary, not a perl script), and it doesn't accept data from stdin.
@CpnCrunch Same in RHEL 6.2 and Cygwin (rename --version says rename from util-linux 2.x.x, but a good tool for mass renaming anyway
|
128
find . -depth -name '* *' \
| while IFS= read -r f ; do mv -i "$f" "$(dirname "$f")/$(basename "$f"|tr ' ' _)" ; done

failed to get it right at first, because I didn't think of directories.

12 Comments

Dennis, good catch, easily fixed by putting IFS='' in front of read. Also, for what I can tell by other comments, sort step can be dropped in favor of -depth option to find.
Does no't work if a filename contain a \ (backslash). Can be fixed by adding a -r option to read.
This must be the 50th time I visit this page to copy and use your solution. Thank you very much. I prefer your answer, as I am on a Mac and do not have the rename command suggested by Dennis.
@mtk, it's a glob pattern, basically "anything-space-anything". Could also be reworded as "anything containing space".
The mv -i fails if there is an existing file with that name, because it reads from stdin which is redirected. It seems writing mv -i ... </dev/tty solves this and the prompt works.
|
55

you can use detox by Doug Harple

detox -r <folder>

1 Comment

best solution, because it is the simplist. btw, the link changed to github.com/dharple/detox but I am on arch, and this is already in Arch repo, so using pacman command installs it easily. No need to build it from sources.
16

A find/rename solution. rename is part of util-linux.

You need to descend depth first, because a whitespace filename can be part of a whitespace directory:

find /tmp/ -depth -name "* *" -execdir rename " " "_" "{}" ";"

6 Comments

I get no change at all when I run yours.
Check util-linux setup: $ rename --version rename (util-linux-ng 2.17.2)
Grepping /usr/bin/rename (a Perl script) reveals "Revision 1.5 1998/12/18 16:16:31 rmb1"
It's named rename.ul on my system.
which only changes one space in my run, so "go tell fire on the mountain" becomes "go_tell fire on the mountain".
|
13

you can use this:

find . -depth -name '* *' | while read fname 

do
        new_fname=`echo $fname | tr " " "_"`

        if [ -e $new_fname ]
        then
                echo "File $new_fname already exists. Not replacing $fname"
        else
                echo "Creating new file $new_fname to replace $fname"
                mv "$fname" $new_fname
        fi
done

1 Comment

See the other answers using find, you should include the -depth flag to find. Otherwise you may rename directories before the files in the directories. Same issue with dirname and basename so you don't try to rename dir one/file two in one step.
7

bash 4.0

#!/bin/bash
shopt -s globstar
for file in **/*\ *
do 
    mv "$file" "${file// /_}"       
done

13 Comments

Looks like this will do a mv to itself if a file or directory name has no space in it (mv: cannot move a' to a subdirectory of itself, a/a')
don't matter. just remove the error message by redirecting to /dev/null.
ghostdog, spawning mv fifty five thousands times only to rename four files may be a bit of overhead even if you don't flood user with messages.
krelin, even find will go through those 55000 files you mentioned to find those with spaces and then do the rename. At the back end, its still going through all. If you want, an initial check for spaces before rename will do it .
I was talking about spawning mv, not going through. Wouldn't for file in *' '* or some such do a better job?
|
5

Recursive version of Naidim's Answers.

find . -name "* *" | awk '{ print length, $0 }' | sort -nr -s | cut -d" " -f2- | while read f; do base=$(basename "$f"); newbase="${base// /_}"; mv "$(dirname "$f")/$(basename "$f")" "$(dirname "$f")/$newbase"; done

Comments

5

In macOS

Just like the chosen answer.

brew install rename

# 
cd <your dir>
find . -name "* *" -type d | rename 's/ /_/g'    # do the directories first
find . -name "* *" -type f | rename 's/ /_/g'

Comments

3

For those struggling through this using macOS, first install all the tools:

 brew install tree findutils rename

Then when needed to rename, make an alias for GNU find (gfind) as find. Then run the code of @Michel Krelin:

alias find=gfind 
find . -depth -name '* *' \
| while IFS= read -r f ; do mv -i "$f" "$(dirname "$f")/$(basename "$f"|tr ' ' _)" ; done   

1 Comment

find . -depth -name '* *' \ | while IFS= read -r f ; do mv -i "$f" "$(dirname "$f")/$(basename "$f"|tr ' ' _)" ; done was the only solution that worked for me on Alpine Linux
3

An easy alternative to recursive version is to increase the range of for loop step by step(n times for n sub-levels irrespective of number of sub-directories at each level). i.e from the outermost directory run these.

for f in *; do mv "$f" "${f// /_}"; done 

for f in */*; do mv "$f" "${f// /_}"; done 

for f in */*/*; do mv "$f" "${f// /_}"; done 

To check/understand what's being done, run the following before and after the above steps.

for f in *;do echo $f;done 

for f in */*;do echo $f;done 

for f in */*/*;do echo $f;done 

Comments

2

Here's a (quite verbose) find -exec solution which writes "file already exists" warnings to stderr:

function trspace() {
   declare dir name bname dname newname replace_char
   [ $# -lt 1 -o $# -gt 2 ] && { echo "usage: trspace dir char"; return 1; }
   dir="${1}"
   replace_char="${2:-_}"
   find "${dir}" -xdev -depth -name $'*[ \t\r\n\v\f]*' -exec bash -c '
      for ((i=1; i<=$#; i++)); do
         name="${@:i:1}"
         dname="${name%/*}"
         bname="${name##*/}"
         newname="${dname}/${bname//[[:space:]]/${0}}"
         if [[ -e "${newname}" ]]; then
            echo "Warning: file already exists: ${newname}" 1>&2
         else
            mv "${name}" "${newname}"
         fi
      done
  ' "${replace_char}" '{}' +
}

trspace rootdir _

Comments

2

This one does a little bit more. I use it to rename my downloaded torrents (no special characters (non-ASCII), spaces, multiple dots, etc.).

#!/usr/bin/perl

&rena(`find . -type d`);
&rena(`find . -type f`);

sub rena
{
    ($elems)=@_;
    @t=split /\n/,$elems;

    for $e (@t)
    {
    $_=$e;
    # remove ./ of find
    s/^\.\///;
    # non ascii transliterate
    tr [\200-\377][_];
    tr [\000-\40][_];
    # special characters we do not want in paths
    s/[ \-\,\;\?\+\'\"\!\[\]\(\)\@\#]/_/g;
    # multiple dots except for extension
    while (/\..*\./)
    {
        s/\./_/;
    }
    # only one _ consecutive
    s/_+/_/g;
    next if ($_ eq $e ) or ("./$_" eq $e);
    print "$e -> $_\n";
    rename ($e,$_);
    }
}

Comments

1

I found around this script, it may be interesting :)

 IFS=$'\n';for f in `find .`; do file=$(echo $f | tr [:blank:] '_'); [ -e $f ] && [ ! -e $file ] && mv "$f" $file;done;unset IFS

1 Comment

Fails on files with newlines in their name.
0

Here's a reasonably sized bash script solution

#!/bin/bash
(
IFS=$'\n'
    for y in $(ls $1)
      do
         mv $1/`echo $y | sed 's/ /\\ /g'` $1/`echo "$y" | sed 's/ /_/g'`
      done
)

1 Comment

0

This only finds files inside the current directory and renames them. I have this aliased.

find ./ -name "* *" -type f -d 1 | perl -ple '$file = $_; $file =~ s/\s+/_/g; rename($_, $file);

Comments

0

I just make one for my own purpose. You may can use it as reference.

#!/bin/bash
cd /vzwhome/c0cheh1/dev_source/UB_14_8
for file in *
do
    echo $file
    cd "/vzwhome/c0cheh1/dev_source/UB_14_8/$file/Configuration/$file"
    echo "==> `pwd`"
    for subfile in *\ *; do [ -d "$subfile" ] && ( mv "$subfile" "$(echo $subfile | sed -e 's/ /_/g')" ); done
    ls
    cd /vzwhome/c0cheh1/dev_source/UB_14_8
done

Comments

0

For files in folder named /files

for i in `IFS="";find /files -name *\ *`
do
   echo $i
done > /tmp/list


while read line
do
   mv "$line" `echo $line | sed 's/ /_/g'`
done < /tmp/list

rm /tmp/list

Comments

0

My solution to the problem is a bash script:

#!/bin/bash
directory=$1
cd "$directory"
while [ "$(find ./ -regex '.* .*' | wc -l)" -gt 0 ];
do filename="$(find ./ -regex '.* .*' | head -n 1)"
mv "$filename" "$(echo "$filename" | sed 's|'" "'|_|g')"
done

just put the directory name, on which you want to apply the script, as an argument after executing the script.

Comments

0

Use below command to replace space with underscore in filename as well as directory name.

find -name "* *" -print0 | sort -rz | \
  while read -d $'\0' f; do mv -v "$f" "$(dirname "$f")/$(basename "${f// /_}")"; done

Comments

0

If you need to rename only files in one directory by replacing all spaces. Then you can use this command with rename.ul:

for i in *' '*; do rename.ul ' ' '_' *; done

Comments

0

Actually, there's no need to use rename script in perl:

find . -depth -name "* *" -execdir bash -c 'mv "$1" `echo $1 | sed s/ /_/g`' -- {} \;

Comments

0

use fd and rename fd find all files recursively, then use rename to replace space with _

fd -X rename 's/ /_/g' {}

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.