Replace dots with underscores in filenames, leaving extension intact

I have a bash script which I’m trying to get to replace dots in filenames and replace them with underscores, leaving the extension intact (I’m on Centos 6 btw). As you can see from the output below, the script works when there is a dot to replace, but in cases where the only dot is the extension, the script still tries to rename the file, instead of ignoring it. Can anyone point out how I should handle this better? Thanks for any help.

My (faulty) script:

#!/bin/bash

for THISFILE in *
do
  filename=${THISFILE%.*}
  extension=${THISFILE##*.}
  newname=${filename//./_}
  echo "mv $THISFILE ${newname}.${extension}"
  #mv $THISFILE ${newname}.${extension}
done

Sample input:

1.3MN-Pin-Eurotunnel-Stw505.51.024-EGS-130x130.jpg
Wear-Plates.jpg

Output:

mv 1_3MN-Pin-Eurotunnel-Stw505_51_024-EGS1-130x130.jpg 1_3MN-Pin-Eurotunnel-Stw505_51_024-EGS1-130x130.jpg
mv Wear-Plates_jpg.Wear-Plates_jpg Wear-Plates_jpg.Wear-Plates_jpg
Asked By: bsod99

||

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'
Answered By: Stéphane Chazelas

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!

Answered By: Joseph R.

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.

Answered By: Omnifarious

Looks like some good answers are already available, but here is another using tr and sed:

#!/bin/bash

for file in *; do
    newname=$(echo $file | tr '.' '_' | sed 's/(.*)_([^_]*)$/1.2/g')
    [ "$newname" != "$file" ] && mv "$file" "$newname"
done
Answered By: J.C. Yamokoski

This version lets you explicitly select the number of dots you want to keep, starting from the right-hand side.

Also it will replace and/or delete other characters in addition to dots, and the replacement character is - instead of an underscore, but this can be easily changed.

#!/bin/sh
# Rename files by replacing Unix-unfriendly characters.

usage () {
    cat <<EOF
usage: $0 [OPTIONS] [--] [FILE [FILE...]]
Rename files by replacing Unix-unfriendly characters.

Options:
 -p N              preserve last N dots in filename, or keep all
                   dots if N < 0 (default: 1)
       --help      show this help and exit
EOF
}

error () {
    printf "%sn" "$1" 1>&2
}

delete_chars="()[]{}*?!^~%\<>&$#|'`""
replace_chars=" _.,;-"

unixify_string () (
    printf '%sn' "$1" 
        | tr -d "$delete_chars" 
        | tr -s "$replace_chars" - 
        | to_lower 
        | sed 's/^-(.)/1/; s/(.)-$/1/'
)

to_lower () {
    sed 's/.*/L&/'
}

split () (
    # split '.x.x.x.x'  0 -> '/x.x.x.x.x
    # split '.x.x.x.x'  1 -> '/x.x.x.x/x
    # split '.x.x.x.x'  2 -> '/x.x.x/x/x
    # split '.x.x.x.x' -1 -> '/x/x/x/x/x
    nf=$(printf '%sn' "$1" | tr -d -C . | wc -c)
    if [ $2 -lt 0 ]; then
        keep=0
    else
        keep=$((nf-$2))
    fi
    IFS=. i=0 out= sep=
    for part in $1; do
        out="$out$sep$part"
        if [ -z "$out" -o $i -ge $keep ]; then
            sep=/
        else
            sep=.
        fi
        i=$(($i+1))
    done
    printf '%sn' "$out"
)

unixify () (
    IFS=/ out= sep=
    for part in $(split "$1" $2); do
        out="$out$sep$(unixify_string "$part")"
        sep=.
    done
    printf '%sn' "$out"
)

rename_maybe () (
    dir="$(dirname "$1")"
    name="$(basename "$1")"
    newname="$(unixify "$name" $2)"
    if [ "$newname" != "$name" ]; then
        mv -i "$dir/$name" "$dir/$newname"
    fi
)

# command line arguments

short_opts=p:
long_opts=help

args="$(LC_ALL=C getopt -n "$0" -s sh -o $short_opts -l $long_opts -- "$@")"
if [ $? -eq 0 ]; then
    eval set -- "$args"
else
    exit 1
fi

p=
while [ $# -gt 0 ]; do
    case "$1" in
        --help)
            usage; exit 0 ;;
        -p)
            p="$2"; shift
            if ! [ "$p" -eq "$p" ] 2> /dev/null; then
                error "$0: option requires integer argument -- 'p'"
                exit 1
            fi ;;
        --)
            shift; break ;;
        -*)
            error "$0: illegal option -- '$1'"
            exit 1 ;;
        *)
            break
    esac
    shift
done

# defaults
p=${p:-1}

# echo p=$p
# echo "$@"
# echo n=$#
# exit

if [ $# -lt 1 ]; then
    error "$0: required non-option argument missing"
    exit 1
fi

for file in "$@"; do
    rename_maybe "$file" $p
done
Answered By: Ernest A
@echo off
for /f "delims=" %%a in ('dir /b *') do call :dot "%%a"
pause
goto :EOF
:dot
set "var=%~n1"
set "var=%var:.= %"
ren %1 "%var%%~x1"

@echo off

for /R %location% %%A in (*) do call :repl "%%A"
goto :eof 
:repl
set "_fn=%~nx1"
ren %1 "%_fn: =_%"
Answered By: mdraqeeb
Categories: Answers Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.