345

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
codeforester
  • 39,467
  • 16
  • 112
  • 140
armandino
  • 17,625
  • 17
  • 69
  • 81
  • 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 Apr 25 '10 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 Apr 25 '10 at 18:59

22 Answers22

490

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 .

hoijui
  • 3,615
  • 2
  • 33
  • 41
Naidim
  • 6,918
  • 2
  • 23
  • 20
  • 11
    Simple and work in mac. (mac doesnt have `rename`, and its too hard to install this with brew..) – JohnnyJS Nov 30 '14 at 12:33
  • 7
    awesome answer. i used `for d in *\ *; do mv "$d" "${d// /}"; done` non under score. – Yoon Lee Feb 24 '15 at 06:32
  • 1
    Unlike the 'find -name' answer, this one worked on my OS X! Thank you sir! – Julio Faerman Jan 12 '16 at 14:26
  • 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 Dec 05 '16 at 21:12
  • 1
    worked for me... plus one... Can anyone answer me how "${f// /_}" does this part work? – Mukit09 Jul 17 '17 at 06:14
  • 6
    `${f// /_}` is a [Bash variable expansion](http://wiki.bash-hackers.org/syntax/pe) for [search and replace](https://wiki.bash-hackers.org/syntax/pe#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 May 19 '20 at 00:53
  • 1
    @Ari , thank you so much for explaning all three slashes! The original answer only explains two slashes, which I find very inconvenient. – evaristegd Jun 05 '20 at 15:26
  • 2
    based on your solution, I made it recursive: `while read line ; do mv "$line" "${line// /}" ; done < <(find /path/ -iname "* *")` – Ivan Apolonio May 14 '21 at 20:19
  • 1
    I wanted to remove spaces (as opposed to replacing with _). Removing the underscore in the statement worked: for `f in *\ *; do mv "$f" "${f// /}"; done` – Al Lelopath Aug 24 '21 at 13:36
  • 1
    What does '*\ *' mean? – mtk May 10 '22 at 00:46
  • Any files with a space. * are wildcards and \ escapes the space – Naidim Jul 18 '23 at 21:07
387

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' "{}" \;
Rune Kaagaard
  • 6,643
  • 2
  • 38
  • 29
Dennis Williamson
  • 346,391
  • 90
  • 374
  • 439
  • 7
    No need for two steps: Use Depth-first search: find dir -depth – Jürgen Hötzel Apr 25 '10 at 20:01
  • hmm.. Dennis, what would happen if you have "a a", "a a/b b" directories? Wouldn't it try to rename "a a" to "a_a" and then "a a/b b" (which doesn't exist anymore) to "a_a/b_b"? – Michael Krelin - hacker Apr 25 '10 at 20:53
  • 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/ /_/` ;-) – Michael Krelin - hacker Apr 26 '10 at 14:45
  • Micro-optimization. In my tests there was negligible difference in speed. `time for i in {1..2000}; do echo "a b c d [repeated to a length of 320 characters]" | perl -pe 'y/ abcdefghi/_ABCDEFGHI/' >/dev/null; done` compared to `'s/ /_/g; s/a/A/g; s/b/B/g; s/c/C/g; s/d/D/g; s/e/E/g; s/f/F/g; s/g/G/g; s/h/H/g; s/i/I/g'`. Of course, the transliteration command has its advantages, but so does the substitute command. – Dennis Williamson Apr 26 '10 at 15:51
  • 1
    Of course you're not going to get a performance boost from it. It's more about using the right tool. And this whole question is about micro-optimizing more or less. Isn't it fun, after all? ;-) – Michael Krelin - hacker Apr 26 '10 at 18:33
  • 25
    If you're running this on OS X, you'll need to `brew install rename` – loeschg Aug 08 '14 at 17:54
  • Note that the command has a trailing `;` issue when aliased in a bash profile. But it is totally fine in a bash profile function. – PhysicalChemist Sep 24 '14 at 16:18
  • @PhysicalChemist: Can you be more specific? – Dennis Williamson Sep 24 '14 at 16:28
  • Sure. Bash Profile Alias: `alias space_to_under='find . -depth -name "* *" -execdir rename 's/ /_/g' "{}" \;' ` and the Terminal Output after sourcing `-bash: alias: /_/g "{}" \;: not found` – PhysicalChemist Sep 24 '14 at 16:42
  • 1
    @PhysicalChemist: You have single quotes inside your alias. Try `alias space_to_under='find . -depth -name "* *" -execdir rename "s/ /_/g" "{}" \;'` or `alias space_to_under='find . -depth -name "* *" -execdir rename '\''s/ /_/g'\'' "{}" \;'` – Dennis Williamson Sep 24 '14 at 16:49
  • 1
    You forgot to specify . in the find command so there is no directory root to search. –  Mar 07 '15 at 17:31
  • @FredConcklin: In GNU `find` . (the current directory) is the default search root. – Dennis Williamson Mar 07 '15 at 23:06
  • 10
    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 Nov 04 '15 at 03:03
  • 1
    @CpnCrunch: The Perl script is very short and various versions are available on CPAN and elsewhere. – Dennis Williamson Nov 04 '15 at 20:07
  • 2
    @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 Dec 14 '16 at 11:32
  • `rename` seems to be the most incompatible script (fedora don't work, custom ubuntu half does) - could someone please include a list of working rename versions.... – Wilf Jan 18 '17 at 14:52
  • 1
    On OS X, first do this: https://stackoverflow.com/a/15003450/2008463 $brew install findutils rename $ alias find=gfind – Mohamed El-Nakeep Sep 04 '17 at 10:39
  • you cannot use `rename` on macOS unless you explicitly install it – Sergiu Mar 14 '18 at 08:12
  • 1
    The question is tagged linux and the issue of OS X installation has been addressed in previous comments. – Dennis Williamson Mar 14 '18 at 18:32
  • @CpnCrunch, as the answer says, the utility is sometimes named `prename`. I found "prename" using Yum and it works as advertised. Now, writing a portable script, that is a whole different matter... – phantom-99w Jul 11 '19 at 16:08
121
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.

Michael Krelin - hacker
  • 138,757
  • 24
  • 193
  • 173
  • 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`. – Michael Krelin - hacker Apr 25 '10 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 Jan 12 '13 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. – Alex Constantin Dec 03 '13 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 ;) – Michael Krelin - hacker Dec 04 '13 at 08:13
  • For this particular task, installing anything extra seemed like cheating :) Also, I prefer `homebrew` – Alex Constantin Dec 05 '13 at 15:26
  • @AlexConstantin, agree on the first part and as for the `brew` — in this context it's synonymous to `macports` ;-) – Michael Krelin - hacker Dec 05 '13 at 22:07
  • That't kind of solution i like the most - works eyerywhere without instaling anything :) That's really important for me, because i'm using 3 different OS(Mac OS X, Windows with cygwin, Debian) and i don't like to think before each operation "did i installed this util here?" :) Thank you very much! :) – cyriel Feb 09 '14 at 13:54
  • it works perfectly, I have only had to replace 'mv -i' by 'mv -f' to avoid the overwrite question. Many thanks. – Jose M Lechon Jul 25 '14 at 11:59
  • Works like a charm. Thanks Michael! this is the only one works in this page! – Gary Dec 05 '14 at 18:55
  • Worked beautifully. Thanks! –  Aug 17 '16 at 17:05
  • What does '* *' mean? – mtk May 10 '22 at 00:46
  • 1
    @mtk, it's a glob pattern, basically "anything-space-anything". Could also be reworded as "anything containing space". – Michael Krelin - hacker May 10 '22 at 08:00
48

you can use detox by Doug Harple

detox -r <folder>
user78274
  • 581
  • 4
  • 3
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 " " "_" "{}" ";"
Jürgen Hötzel
  • 18,997
  • 3
  • 42
  • 58
11

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
hoijui
  • 3,615
  • 2
  • 33
  • 41
Itamar
  • 129
  • 1
  • 4
  • 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. – Jason Harrison Jul 20 '21 at 17:52
7

bash 4.0

#!/bin/bash
shopt -s globstar
for file in **/*\ *
do 
    mv "$file" "${file// /_}"       
done
ghostdog74
  • 327,991
  • 56
  • 259
  • 343
  • 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 Apr 26 '10 at 02:50
  • don't matter. just remove the error message by redirecting to `/dev/null`. – ghostdog74 Apr 26 '10 at 03: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. – Michael Krelin - hacker Apr 26 '10 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 Apr 26 '10 at 10:55
  • I was talking about spawning mv, not going through. Wouldn't `for file in *' '*` or some such do a better job? – Michael Krelin - hacker Apr 26 '10 at 11:49
  • that syntax doesn't recurse directory – ghostdog74 Apr 26 '10 at 12:31
  • Your edit is exactly the "somesuch" I was talking about. ;-) I didn't know what `**` is, anyway. Now that you explained that your code expands into the list of all files I think it may turn out to be way too resource consuming for otherwise bearable directory tree ;-) – Michael Krelin - hacker Apr 26 '10 at 12:54
  • its the same as using find to recurse. – ghostdog74 Apr 26 '10 at 13:34
  • no, the memory consumption differs dramatically. With find you stream the results and process them one by one, with expansion you hold the whole thing in memory. – Michael Krelin - hacker Apr 26 '10 at 13:37
  • if OP has to start searching from /, then maybe not this method. Otherwise, its alright. And its definitely faster than your find+basename+tr solution on a given directory. lastly If you find this method not to your taste, then don't use it. Its just an alternative for OP. – ghostdog74 Apr 26 '10 at 14:05
  • What does it have to do with `/`? My openembedded build directory, for instance, has 898199 files (just counted). My solution may be slower, right, I needed that to handle the situation described in my comment to Dennis' answer. Note, that your solution suffers the same problem. Otherwise, yes, your solution would be valid even if I don't like it. – Michael Krelin - hacker Apr 26 '10 at 14:21
  • when i say "/", i mean if OP is searching the whole file system recursively..ie /etc/, /var/, /tmp, /usr/, /opt...and any other directories under "/". Then it might not be advisable to use globstar. – ghostdog74 Apr 26 '10 at 23:51
  • The bash string manipulation way `${file// /_}` shown here is still something that makes this answer worthwhile. – Anaphory Apr 03 '13 at 16:21
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'

C.K.
  • 4,348
  • 29
  • 43
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
Junyeop Lee
  • 237
  • 2
  • 10
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   
petezurich
  • 9,280
  • 9
  • 43
  • 57
Mohamed El-Nakeep
  • 6,580
  • 4
  • 35
  • 39
  • `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 Jul 28 '20 at 12:41
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 _
yabt
  • 21
  • 1
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,$_);
    }
}
Jason Plank
  • 2,336
  • 5
  • 31
  • 40
degi
  • 21
  • 1
2

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 
1

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
)
Community
  • 1
  • 1
jojohtf
  • 19
  • 1
  • 2
    [Never parse the output of `ls`.](http://mywiki.wooledge.org/BashPitfalls#for_i_in_.24.28ls_.2A.mp3.29) – ghoti Dec 05 '16 at 21:17
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
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
Hongtao
  • 197
  • 1
  • 5
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
Vaibhav Panmand
  • 349
  • 4
  • 10
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

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

Andro Selva
  • 53,910
  • 52
  • 193
  • 240