Replace all dots in filenames with underscore

filenamesrename

I have a directory with some subdirectories with filenames and directorynames that contain some dots:

$ ls -R
.:
dir  file.with.dots

./dir:
subdir.with.dots

./dir/subdir.with.dots:
other.file

I really struggle to catch all these files with find because I don't want to rename the current folder . and .. itself.

I also read how to Replace dots with underscores in filenames, leaving extension intact, but that doesn't really fit here, because the main problem is the recursive selection of the files.

I tried to replace the dots with find and tr:

find $(pwd) -depth -name '*.*'|while read f; do mv -iv "$f" '"'$(echo $f|tr '.' '_')'"' ; done

but this would give errors if in both, the path to the file and the file itself, contains dots

How do I rename and replace all occurrences of a dot with an underscore?

Best Answer

With zsh:

autoload zmv
zmv '(**/)(*.*)' '$1${2//./_}'

Otherwise, if you have access to bash (though ksh93 or zsh will do as well), you could always do:

find . -depth ! -name '.*' -name '*.*' -exec bash -c '
  for file do
    dir=${file%/*}
    base=${file##*/}
    mv -i -- "$file" "$dir/${base//./_}"
  done' sh {} +

(though you'll miss the sanity checks done by zmv).

Those (intentionally) don't rename hidden files.

The point is to process the list depth-first (which zmv does by default, and using -depth in find) and only rename the basename of the file.

So that you do:

mv -- a.b/c.d a.b/c_d

and then:

mv -- a.b a_b

Also note that for the non-zmv solution, if you have a a.b file and a a_b directory in the same directory, then mv -- a.b a_b will happily move a.b into a_b/. To avoid that, if on a GNU system, you can add the -T option to mv.


As I see you tried to edit the answer to use a while read loop.

Note that you should generally avoid while read loops, even in this case where one command per line has to be run anyway. If you really want to use one, then doing it right is tricky and involves ksh/bash/zshisms:

{
  find . -depth ! -name '.*' -name '*.*' -exec printf '%s\0' {} + |
    while IFS= read -rd '' file; do
      dir=${file%/*}
      base=${file##*/}
      mv -i -- "$file" "$dir/${base//./_}"
    done <&3 3<&-
} 3<&0

(you can replace -exec printf '%s\0' {} + with -print0 if your find supports it).

You need:

  • -print0/-exec printf '%s\0' {} + as you can't rely on newline as the separator since newline is a valid character in a filename
  • as a result, you need -d '' (read reads until NUL instead of NL)
  • IFS= (that is remove all the whitespace characters from IFS for read as otherwise strips them from the end of the input record).
  • -r as otherwise read treats backslash as an escape character (for the field and record separator).
  • that litte dance with file descriptors because you want the standard input of mv to be the terminal (for the answers to the -i prompts), not the pipe from find.
Related Question