8

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
  • 1
    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 '14 at 23:33
10

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.

  • This does indeed do the job nicely :) Thanks for the explanation too re the shell expansion. – bsod99 Nov 27 '12 at 9:58
  • In addition to replacing dot (.) with (_), how can I delete spaces in the filenames. – discipulus Apr 19 '16 at 11:11
  • This is a very nice script. The only improvements I can see is to make it recurse a directory tree and to let you replace $1 with $2, but those are minor. (Or, as my teacher used to say, "left as an exercise for the student"!) – lbutlr Jan 28 '17 at 18:42
4

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

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!

  • 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
2

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
  • 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
1

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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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