Convert all file extensions to lower-case
Solution 1:
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.
Solution 2:
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
Solution 3:
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
Solution 4:
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...