352

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?
    – Mark Byers
    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.
    – armandino
    Commented Apr 25, 2010 at 18:59

23 Answers 23

501

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 .

11
  • 11
    Simple and work in mac. (mac doesnt have rename, and its too hard to install this with brew..)
    – JohnnyJS
    Commented Nov 30, 2014 at 12:33
  • 7
    awesome answer. i used for d in *\ *; do mv "$d" "${d// /}"; done non under score.
    – Yoon Lee
    Commented Feb 24, 2015 at 6:32
  • 2
    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.
    – ghoti
    Commented Dec 5, 2016 at 21:12
  • 6
    ${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"
    – Ari
    Commented May 19, 2020 at 0:53
  • 2
    based on your solution, I made it recursive: while read line ; do mv "$line" "${line// /}" ; done < <(find /path/ -iname "* *") Commented May 14, 2021 at 20:19
391

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' "{}" \;
20
  • 7
    No need for two steps: Use Depth-first search: find dir -depth Commented Apr 25, 2010 at 20:01
  • 4
    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/ /_/ ;-) Commented Apr 26, 2010 at 14:45
  • 25
    If you're running this on OS X, you'll need to brew install rename
    – loeschg
    Commented Aug 8, 2014 at 17:54
  • 12
    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
    Commented Nov 4, 2015 at 3:03
  • 3
    @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
    – golimar
    Commented Dec 14, 2016 at 11:32
124
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.

10
  • 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. Commented Apr 25, 2010 at 20:10
  • 1
    Does no't work if a filename contain a \ (backslash). Can be fixed by adding a -r option to read.
    – jfg956
    Commented Jan 12, 2013 at 16:35
  • 10
    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. Commented Dec 3, 2013 at 20:51
  • @AlexConstantin, don't macports have the rename? I have never bothered to find out because I don't think the task justifies utility. And if you don't have macports, you should consider installing them ;) Commented Dec 4, 2013 at 8:13
  • 1
    @mtk, it's a glob pattern, basically "anything-space-anything". Could also be reworded as "anything containing space". Commented May 10, 2022 at 8:00
50

you can use detox by Doug Harple

detox -r <folder>
0
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
  • I get no change at all when I run yours. Commented Apr 25, 2010 at 20:24
  • Check util-linux setup: $ rename --version rename (util-linux-ng 2.17.2) Commented Apr 25, 2010 at 21:00
  • Grepping /usr/bin/rename (a Perl script) reveals "Revision 1.5 1998/12/18 16:16:31 rmb1" Commented Apr 25, 2010 at 22:22
  • 1
    It's named rename.ul on my system. Commented Nov 24, 2010 at 17:15
  • 3
    which only changes one space in my run, so "go tell fire on the mountain" becomes "go_tell fire on the mountain".
    – brokkr
    Commented Mar 22, 2012 at 20:33
12

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
  • 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. Commented Jul 20, 2021 at 17:52
7

bash 4.0

#!/bin/bash
shopt -s globstar
for file in **/*\ *
do 
    mv "$file" "${file// /_}"       
done
13
  • 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')
    – armandino
    Commented Apr 26, 2010 at 2:50
  • don't matter. just remove the error message by redirecting to /dev/null.
    – ghostdog74
    Commented Apr 26, 2010 at 3:47
  • 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. Commented Apr 26, 2010 at 10:11
  • 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 .
    – ghostdog74
    Commented Apr 26, 2010 at 10:55
  • I was talking about spawning mv, not going through. Wouldn't for file in *' '* or some such do a better job? Commented Apr 26, 2010 at 11:49
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'

4

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
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
  • 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
    – Lucas
    Commented Jul 28, 2020 at 12:41
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 
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 _
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,$_);
    }
}
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
  • Fails on files with newlines in their name.
    – ghoti
    Commented Dec 5, 2016 at 21:16
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
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);

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

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

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`' -- {} \;
0

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

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

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