Take the 2-minute tour ×
Unix & Linux Stack Exchange is a question and answer site for users of Linux, FreeBSD and other Un*x-like operating systems.. It's 100% free, no registration required.

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
share|improve this question
    
What about tricky cases such as tar.gz files? You would want them to resolve to file.tar.gz, not file_tar.gz. –  IQAndreas Aug 5 at 23:33

5 Answers 5

up vote 2 down vote accepted

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.

share|improve this answer
    
This does indeed do the job nicely :) Thanks for the explanation too re the shell expansion. –  bsod99 Nov 27 '12 at 9:58

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 "%s\n" "$1" 1>&2
}

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

unixify_string () (
    printf '%s\n' "$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 '%s\n' "$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 '%s\n' "$out"
)

unixify () (
    IFS=/ out= sep=
    for part in $(split "$1" $2); do
        out="$out$sep$(unixify_string "$part")"
        sep=.
    done
    printf '%s\n' "$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
share|improve this answer

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!

share|improve this answer
    
Terse, pithy, nearly write-only! Yay perl! chuckle –  Omnifarious Nov 26 '12 at 16:07
    
Write-only? I bet that's the first time Perl has been called that! (chuckle again) –  Joseph R. Nov 26 '12 at 16:13
    
Took a shot at improving readability there. Hope this feels less "write-only" :) –  Joseph R. Nov 26 '12 at 17:22
1  
Yes, that helps a bunch. It's a pity the only reasonable character to use as a placeholder there is /. –  Omnifarious Nov 26 '12 at 18:34

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'
share|improve this answer
    
I may have implemented your suggestion wrongly, but this results in mv -- ..* _.* –  bsod99 Nov 26 '12 at 15:17

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
share|improve this answer
    
How does sed decide which .* to apply maximal munch to? I would feel a lot better about it if the second .* were [^_]*. –  Omnifarious Nov 26 '12 at 16:16
    
I also believe this falls down on the case of 'no extension' and a filename with \t characters in it. –  Omnifarious Nov 26 '12 at 16:19
    
This was definitely just a quick solution. Thanks for your input... I fixed the regex to accommodate your first recommendation. I'll see if I can tweak it to account for the other circumstances you mentioned. –  jonyamo Nov 26 '12 at 16:28
    
echo -n "$file" would work. :-) Oh, and "'s around the $( ... ) expression (aka "$( ... )") would do it. –  Omnifarious Nov 26 '12 at 17:15

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.