How to replace spaces in file names using a bash script

LinuxBashWhitespaceFilenames

Linux Problem Overview


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

Linux Solutions


Solution 1 - Linux

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 .

Solution 2 - Linux

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 /tmp/ -depth -name "* *" -execdir rename 's/ /_/g' "{}" \;

Solution 3 - Linux

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.

Solution 4 - Linux

you can use detox by Doug Harple

detox -r <folder>

Solution 5 - Linux

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 " " "_" "{}" ";"

Solution 6 - Linux

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

Solution 7 - Linux

bash 4.0

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

Solution 8 - Linux

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

Solution 9 - Linux

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

Solution 10 - Linux

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   

Solution 11 - Linux

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 _

Solution 12 - Linux

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,$_);
    }
}

Solution 13 - Linux

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
)

Solution 14 - Linux

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

Solution 15 - Linux

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 

Solution 16 - Linux

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

Solution 17 - Linux

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

Solution 18 - Linux

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

Solution 19 - Linux

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.

Solution 20 - Linux

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionarmandinoView Question on Stackoverflow
Solution 1 - LinuxNaidimView Answer on Stackoverflow
Solution 2 - LinuxDennis WilliamsonView Answer on Stackoverflow
Solution 3 - LinuxMichael Krelin - hackerView Answer on Stackoverflow
Solution 4 - Linuxuser78274View Answer on Stackoverflow
Solution 5 - LinuxJürgen HötzelView Answer on Stackoverflow
Solution 6 - LinuxItamarView Answer on Stackoverflow
Solution 7 - Linuxghostdog74View Answer on Stackoverflow
Solution 8 - LinuxJunyeop LeeView Answer on Stackoverflow
Solution 9 - LinuxC.K.View Answer on Stackoverflow
Solution 10 - LinuxMohamed El-NakeepView Answer on Stackoverflow
Solution 11 - LinuxyabtView Answer on Stackoverflow
Solution 12 - LinuxdegiView Answer on Stackoverflow
Solution 13 - LinuxjojohtfView Answer on Stackoverflow
Solution 14 - LinuxJuan Sebastian ToteroView Answer on Stackoverflow
Solution 15 - LinuxNikhil UlahannanView Answer on Stackoverflow
Solution 16 - Linuxuser1060059View Answer on Stackoverflow
Solution 17 - LinuxHongtaoView Answer on Stackoverflow
Solution 18 - LinuxMarcos Jean SampaioView Answer on Stackoverflow
Solution 19 - LinuxÁgoston VolczView Answer on Stackoverflow
Solution 20 - LinuxVaibhav PanmandView Answer on Stackoverflow