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
  • 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
    Commented Aug 5, 2014 at 23:33

6 Answers 6

9

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.

3
  • This does indeed do the job nicely :) Thanks for the explanation too re the shell expansion.
    – bsod99
    Commented Nov 27, 2012 at 9:58
  • In addition to replacing dot (.) with (_), how can I delete spaces in the filenames.
    – discipulus
    Commented Apr 19, 2016 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
    Commented Jan 28, 2017 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'
1
  • I may have implemented your suggestion wrongly, but this results in mv -- ..* _.*
    – bsod99
    Commented Nov 26, 2012 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!

4
  • Terse, pithy, nearly write-only! Yay perl! chuckle Commented Nov 26, 2012 at 16:07
  • Write-only? I bet that's the first time Perl has been called that! (chuckle again)
    – Joseph R.
    Commented Nov 26, 2012 at 16:13
  • Took a shot at improving readability there. Hope this feels less "write-only" :)
    – Joseph R.
    Commented Nov 26, 2012 at 17:22
  • 1
    Yes, that helps a bunch. It's a pity the only reasonable character to use as a placeholder there is /. Commented Nov 26, 2012 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
4
  • How does sed decide which .* to apply maximal munch to? I would feel a lot better about it if the second .* were [^_]*. Commented Nov 26, 2012 at 16:16
  • I also believe this falls down on the case of 'no extension' and a filename with \t characters in it. Commented Nov 26, 2012 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. Commented Nov 26, 2012 at 16:28
  • echo -n "$file" would work. :-) Oh, and "'s around the $( ... ) expression (aka "$( ... )") would do it. Commented Nov 26, 2012 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
-2
@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: =_%"
2
  • Bash Script for windows replace dots with underscores in filenames, leaving the extension,
    – mdraqeeb
    Commented May 10, 2022 at 12:40
  • "Bash script for Windows"? This does not look like a bash script at all. The user states that they're using a Linux distribution, and this is the "Unix & Linux" site. What makes you think they are using Windows? Also, since this is a script provided entirely without comments, instructions, examples or failure conditions, in an unfamiliar scripting language, I would advice anyone to try to run it.
    – Kusalananda
    Commented May 10, 2022 at 17:21

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .