views:

571

answers:

5

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

Edit

Thanks for the answers; they all seem to work. I picked the one by Dennis as the main answer because it seems to me the simplest, even though it takes two steps.

+5  A: 
find . -name '* *' -depth \
| while read 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
Works like a charm. Thanks Michael.
armandino
Doesn't work if the filename has a trailing space.
Dennis Williamson
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
+4  A: 

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'

Edit:

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' "{}" \;
Dennis Williamson
No need for two steps: Use Depth-first search: find dir -depth
Jürgen Hötzel
@Jürgen: not with a pipe like I've shown.
Dennis Williamson
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
@Michael: You are correct. I've edited my answer.
Dennis Williamson
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
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
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
+1  A: 

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
I get no change at all when I run yours.
Dennis Williamson
Check util-linux setup:$ rename --versionrename (util-linux-ng 2.17.2)
Jürgen Hötzel
Grepping /usr/bin/rename (a Perl script) reveals "Revision 1.5 1998/12/18 16:16:31 rmb1"
Dennis Williamson
Hmm... where is your util-linux binary gone? This file path should be owned by util-linux. You don't us a GNU-Linux system?
Jürgen Hötzel
A: 

bash 4.0

#!/bin/bash
shopt -s globstar
for file in **/*\ *
do 
    mv "$file" "${file// /_}"       
done
ghostdog74
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
don't matter. just remove the error message by redirecting to `/dev/null`.
ghostdog74
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
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
I was talking about spawning mv, not going through. Wouldn't `for file in *' '*` or some such do a better job?
Michael Krelin - hacker
that syntax doesn't recurse directory
ghostdog74
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
its the same as using find to recurse.
ghostdog74
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
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
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
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
A: 

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