I believe that this program will do what you want. I have tested it and it works on several interesting cases (such as no extension at all):
#!/bin/bash
for fname in *; do
name="${fname%\.*}"
extension="${fname#$name}"
newname="${name//./_}"
newfname="$newname""$extension"
if [ "$fname" != "$newfname" ]; then
echo mv "$fname" "$newfname"
#mv "$fname" "$newfname"
fi
done
The main issue you had was that the ##
expansion wasn't doing what you wanted. I've always considered shell parameter expansion in bash to be something of a black art. The explanations in the manual are not completely clear, and they lack any supporting examples of how the expansion is supposed to work. They're also rather cryptic.
Personally, I would've written a small script in sed
that fiddled the name the way I wanted, or written a small script in perl
that just did the whole thing. One of the other people who answered took that approach.
One other thing I would like to point out is my use of quoting. Every time I do anything with shell scripts I remind people to be very careful with their quoting. A huge source of problems in shell scripts is the shell interpreting things it's not supposed to. And the quoting rules are far from obvious. I believe this shell script is free of quoting issues.
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
You don't really need the
rename
command for this, you can do it directly in the shell:The
printf "%03d" $c
will print the$c
variable, padding it with 3 leading 0s as needed. Thelet c++
increments the counter ($c
). The${i#"${i%.*}"}
extracts the extension. More on that here.I would not use
rename
for this. I don't think it can do calculations. Valid Perl constructs likes/.*/$c++/e
fail and I don't see any other way to have it do calculations. That means you would still need to run it in a loop and have something else increment the counter for you. Something like:But there's no advantage over using the simpler
mv
approach.