Replace dots with underscores in filenames, leaving extension intact

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.


Use for thisfile in *.*.* (that is, loop over files with two dots or more in their name). Remember to quote your variables and use -- to mark the end of options as in mv -- "$thisfile" "$newname.$extension"

With zsh.

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

How about this:

perl -e '
         @files = grep {-f} glob "*";
         @old_files = @files;
         map {
              s!(.*)\.!$1/!;
              s!\.!_!g;
              s!/!.!
             } @files;
         rename $old_files[$_] => $files[$_] for (0..$#files)
        '

DISCLAIMER: try it on a dummy directory first, I haven't tested it!