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
.
Best Answer
If you need to rename files in subdirectories as well, and your
find
supports the-execdir
predicate, then you can doThank to @glenn jackman for suggesting
-depth
option forfind
and to make me think.Note that on some systems (including GNU/Linux ones),
find
may fail to find files whose name contains spaces and also sequences of bytes that don't form valid characters (typical with media files with names with non-ASCII characters encoded in a charset different from the locale's). Setting the locale toC
(as inLC_ALL=C find...
) would address the problem.