thevoipman
thevoipman

Reputation: 1833

Convert all file extensions to lower-case

I'm trying to lower-case all my extensions regardless of what it is. So far, from what I've seen, you have to specify what file extensions you want to convert to lower-case. However, I just want to lower-case everything after the first last dot . in the name.

How can I do that in bash?

Upvotes: 51

Views: 59527

Answers (12)

Łukasz Rajchel
Łukasz Rajchel

Reputation: 331

My small and primitive script to convert extensions to lowercase recursively:

#!/bin/bash

# find files with at least one uppercase character in an extension in the input directory recursively and optionally convert extensions to lowercase

# usage:
# find_file_extensions_with_uppercase_chars dir [convert]

dir=$1
[[ -z $1 ]] && dir=.

shopt -s nullglob dotglob globstar extglob
for file in "$dir"/**/*; do
    if [[ ! -d $file ]]; then
        filename=${file##*/}
        if [[ $filename =~ \. ]]; then
            ext="${filename#*.}"
            if [[ $ext =~ [[:upper:]] ]]; then
                file="${file//+(\/)/\/}"
                echo $file
                if [[ -n $2 ]]; then
                    ext_length=${#ext}
                    conv_file=${file::((-ext_length))}${ext,,}
                    mv -iv "$file" "$conv_file"
                fi
            fi
        fi
    fi
done

Here, an extension is a string after the first dot in the filename, if you want it to be a string after the last dot in the filename (as you've asked), just change ext="${filename#*.}" to ext="${filename##*.}".

Upvotes: 0

Jan D
Jan D

Reputation: 11

I was looking for a simple way to do this (without having to think about it) but I finally ended up thinking it through and came up with this (admittedly, way after the original post)

find . -name \\*.JPG -print -exec rename s/.JPG/.jpg/ {} \\;

I ran it on about 60 thousand files and it worked fine but, of course, you can use the -n option to 'rename' if you want to test it out first.

Upvotes: 1

Michael Leonard
Michael Leonard

Reputation: 1733

If you are using ZSH:

zmv '(*).(*)' '$1.$2:l'

If you get zsh: command not found: zmv then simply run:

autoload -U zmv

And then try again.

Thanks to this original article for the tip about zmv and the ZSH documentation for the lowercase/uppercase substitution syntax.

Upvotes: 9

fikr4n
fikr4n

Reputation: 3420

This is shorter but more general, combined from other's answer:

rename 's/\.([^.]+)$/.\L$1/' *

Simulation

For simulation, use -n, i.e. rename -n 's/\.([^.]+)$/.\L$1/' *. This way you can see what will be changed before the real changes being performed. Example output:

Happy.Family.GATHERING.JPG renamed as Happy.Family.GATHERING.jpg
Hero_from_The_Land_Across_the_River.JPG renamed as Hero_from_The_Land_Across_the_River.jpg
rAnD0m.jPg1 renamed as rAnD0m.jpg1

Short explanation about the syntax

  • The syntax is rename OPTIONS 's/WHAT_TO_FIND_IN_THE_NAME/THE_REPLACEMENT/' FILENAMES
  • \.([^.]+)$ means sequence of anything but dot ([^.]) at the end of the string ($), after dot (\.)
  • .\L$1 means dot (\.) followed by lowercase (\L) of 1st group ($1)
  • First group in this case is the extension ([^.]+)
  • You better use single quote ' instead of double quote " to wrap the regex to avoid shell expansion

Upvotes: 19

balcoder
balcoder

Reputation: 719

If your only interested in certain file extensions like converting all higher case "JPG" extensions to lower case "jpg" You could use the command line utility rename like so. CD into directory you want to change. Then

rename -n 's/\.JPG$/\.jpg/' *

Use -n option to test what will be changed, then when you happy with results use without like so

rename  's/\.JPG$/\.jpg/' *

Upvotes: 0

Igor Chubin
Igor Chubin

Reputation: 64563

Solution

You can solve the task in one line:

find . -name '*.*' -exec sh -c '
  a=$(echo "$0" | sed -r "s/([^.]*)\$/\L\1/");
  [ "$a" != "$0" ] && mv "$0" "$a" ' {} \;

Note: this will break for filenames that contain newlines. But bear with me for now.

Example of usage

$ mkdir C; touch 1.TXT a.TXT B.TXT C/D.TXT
$ find .
.
./C
./C/D.TXT
./1.TXT
./a.TXT
./B.TXT

$ find . -name '*.*' -exec sh -c 'a=$(echo "$0" | sed -r "s/([^.]*)\$/\L\1/"); [ "$a" != "$0" ] && mv "$0" "$a" ' {} \;

$ find .
.
./C
./C/D.txt
./a.txt
./B.txt
./1.txt

Explanation

You find all files in current directory (.) that have period . in its name (-name '*.*') and run the command for each file:

a=$(echo "$0" | sed -r "s/([^.]*)\$/\L\1/");
[ "$a" != "$0" ] && mv "{}" "$a"

That command means: try to convert file extension to lowercase (that makes sed):

$ echo 1.txt | sed -r "s/([^.]*)\$/\L\1/"
1.txt
$ echo 2.TXT | sed -r "s/([^.]*)\$/\L\1/"
2.txt

and save the result to the a variable.

If something was changed [ "$a" != "$0" ], rename the file mv "$0" "$a".

The name of the file being processed ({}) passed to sh -c as its additional argument and it is seen inside the command line as $0. It makes the script safe, because in this case the shell take {} as a data, not as a code-part, as when it is specified directly in the command line. (I thank @gniourf_gniourf for pointing me at this really important issue).

As you can see, if you use {} directly in the script, it's possible to have some shell-injections in the filenames, something like:

; rm -rf * ;

In this case the injection will be considered by the shell as a part of the code and they will be executed.

While-version

Clearer, but a little bit longer, version of the script:

find . -name '*.*' | while IFS= read -r f
do
  a=$(echo "$f" | sed -r "s/([^.]*)\$/\L\1/");
  [ "$a" != "$f" ] && mv "$f" "$a"
done

This still breaks for filenames containing newlines. To fix this issue, you need to have a find that supports -print0 (like GNU find) and Bash (so that read supports the -d delimiter switch):

find . -name '*.*' -print0 | while IFS= read -r -d '' f
do
  a=$(echo "$f" | sed -r "s/([^.]*)\$/\L\1/");
  [ "$a" != "$f" ] && mv "$f" "$a"
done

This still breaks for files that contain trailing newlines (as they will be absorbed by the a=$(...) subshell. If you really want a foolproof method (and you should!), with a recent version of Bash (Bash≥4.0) that supports the ,, parameter expansion here's the ultimate solution:

find . -name '*.*' -print0 | while IFS= read -r -d '' f
do
  base=${f%.*}
  ext=${f##*.}
  a=$base.${ext,,}
  [ "$a" != "$f" ] && mv -- "$f" "$a"
done

Back to the original solution

Or in one find go (back to the original solution with some fixes that makes it really foolproof):

find . -name '*.*' -type f -exec bash -c 'base=${0%.*} ext=${0##*.} a=$base.${ext,,}; [ "$a" != "$0" ] && mv -- "$0" "$a"' {} \;

I added -type f so that only regular files are renamed. Without this, you could still have problems if directory names are renamed before file names. If you also want to rename directories (and links, pipes, etc.) you should use -depth:

find . -depth -name '*.*' -type f -exec bash -c 'base=${0%.*} ext=${0##*.} a=$base.${ext,,}; [ "$a" != "$0" ] && mv -- "$0" "$a"' {} \;

so that find performs a depth-first search.

You may argue that it's not efficient to spawn a bash process for each file found. That's correct, and the previous loop version would then be better.

Upvotes: 88

Govind Totla
Govind Totla

Reputation: 1178

recursively for all one fine solution:

find -name '*.JPG' | sed 's/\(.*\)\.JPG/mv "\1.JPG" "\1.jpg"/' |sh

The above recursively renames files with the extension "JPG" to files with the extension "jpg"

Upvotes: 2

P Sreedhar
P Sreedhar

Reputation: 695

I got success with this command.

rename JPG jpg *.JPG

Where rename is a command that tells the shell to rename every occurrence of JPG to jpg in the current folder with all filenames having extension JPG.

If you see Bareword "JPG" not allowed while "strict subs" in use at (eval 1) line 1 with this approach try:

rename 's/\.JPG$/.jpg/' *.JPG

Upvotes: 62

Jason Sundram
Jason Sundram

Reputation: 12524

So, these solutions that look like line noise are nice and all, but this is easy to do from the python REPL (I know the OP asked for bash, but python is installed on a lot of systems that have bash these days...):

import os
files = os.listdir('.')
for f in files:
    path, ext = os.path.splitext(f)
    if ext.isupper():
        os.rename(f, path + ext.lower())

Upvotes: 0

Elmar Zander
Elmar Zander

Reputation: 1516

If you have mmv (=move multiple files) installed and your filenames contain at most one dot, you can use

mmv -v "*.*" "#1.#l2"

It does not get more than one dot right (since the matching algo for * is not greedy in mmv), however, it handles () and ' correctly. Example:

$ mmv -v "*.*" "#1.#l2"
FOO.BAR.MP3 -> FOO.bar.mp3 : done
foo bar 'baz' (CD 1).MP3 -> foo bar 'baz' (CD 1).mp3 : done

Not perfect, but much easier to use and remember than all the find/exec/sed stuff.

Upvotes: 1

tzelleke
tzelleke

Reputation: 15345

This will do the job for your '.mp3's - but only in the working directory - however is able to consume filenames with whitespace:

for f in *.[mM][pP]3; do mv "$f" "${f%.*}.mp3"; done

Correction:

for f in *.[mM][pP]3; do [[ "$f" =~ \.mp3$ ]] || mv "$f" "${f%.*}.mp3"; done

Upvotes: 5

thkala
thkala

Reputation: 86333

Well, you could use this snippet as the core of whatever alternative you need:

#!/bin/bash

# lowerext.sh    

while read f; do
    if [[ "$f" = *.* ]]; then
        # Extract the basename
        b="${f%.*}"

        # Extract the extension
        x="${f##*.}"

        # Convert the extension to lower case
        # Note: this only works in recent versions of Bash
        l="${x,,}"

        if [[ "$x" != "$l" ]]; then
            mv "$f" "$b.$l"
        fi
    else
        continue
    fi
done

Afterwards, all you need to do is feed a list of the files you need to rename to its standard input. E.g. for all files under the current directory and any subdirectory:

find -type f | lowerext.sh

A small optimization:

find -type f -name '*.*' | lowerext.sh

You will have to be more specific if you need a more concrete answer than this...

Upvotes: 11

Related Questions