How do I change extension of multiple files recursively from the command line?
Solution 1
A portable way (which will work on any POSIX compliant system):
find /the/path -depth -name "*.abc" -exec sh -c 'mv "$1" "${1%.abc}.edefg"' _ {} \;
In bash4, you can use globstar to get recursive globs (**):
shopt -s globstar
for file in /the/path/**/*.abc; do
mv "$file" "${file%.abc}.edefg"
done
The (perl) rename
command in Ubuntu can rename files using perl regular expression syntax, which you can combine with globstar or find
:
# Using globstar
shopt -s globstar
files=(/the/path/**/*.abc)
# Best to process the files in chunks to avoid exceeding the maximum argument
# length. 100 at a time is probably good enough.
# See http://mywiki.wooledge.org/BashFAQ/095
for ((i = 0; i < ${#files[@]}; i += 100)); do
rename 's/\.abc$/.edefg/' "${files[@]:i:100}"
done
# Using find:
find /the/path -depth -name "*.abc" -exec rename 's/\.abc$/.edefg/' {} +
Also see http://mywiki.wooledge.org/BashFAQ/030
Solution 2
This will do the required task if all the files are in the same folder
rename 's/.abc$/.edefg/' *.abc
To rename the files recursively use this:
find /path/to/root/folder -type f -name '*.abc' -print0 | xargs -0 rename 's/.abc$/.edefg/'
Solution 3
One problem with recursive renames is that whatever method you use to locate the files, it passes the whole path to rename
, not just the file name. That makes it hard to do complex renames in nested folders.
I use find
's -execdir
action to solve this problem. If you use -execdir
instead of -exec
, the specified command is run from the subdirectory containing the matched file. So, instead of passing the whole path to rename
, it only passes ./filename
. That makes it much easier to write the regex.
find /the/path -type f \
-name '*.abc' \
-execdir rename 's/\.\/(.+)\.abc$/version1_$1.abc/' '{}' \;
In detail:
-
-type f
means only look for files, not directories -
-name '*.abc'
means only match filenames that end in .abc -
'{}'
is the placeholder that marks the place where-execdir
will insert the found path. The single-quotes are required, to allow it to handle file names with spaces and shell characters. - The backslashes after
-type
and-name
are the bash line-continuation character. I use them to make this example more readable, but they are not needed if you put your command all on one line. - However, the backslash at the end of the
-execdir
line is required. It is there to escape the semicolon, which terminates the command run by-execdir
. Fun!
Explanation of the regex:
-
s/
start of the regex -
\.\/
match the leading ./ that -execdir passes in. Use \ to escape the . and / metacharacters (note: this part vary depending on your version offind
. See comment from user @apollo) -
(.+)
match the filename. The parentheses capture the match for later use -
\.abc
escape the dot, match the abc $
anchor the match at the end of the string/
marks the end of the "match" part of the regex, and the start of the "replace" partversion1_
add this text to every file name-
$1
references the existing filename, because we captured it with parentheses. If you use multiple sets of parentheses in the "match" part, you can refer to them here using $2, $3, etc. -
.abc
the new file name will end in .abc. No need to escape the dot metacharacter here in the "replace" section -
/
end of the regex
Before
tree --charset=ascii
|-- a_file.abc
|-- Another.abc
|-- Not_this.def
`-- dir1
`-- nested_file.abc
After
tree --charset=ascii
|-- version1_a_file.abc
|-- version1_Another.abc
|-- Not_this.def
`-- dir1
`-- version1_nested_file.abc
Hint: rename
's -n option is useful. It does a dry run and shows you what names it will change, but does not make any changes.
Solution 4
Another portable way:
find /the/path -depth -type f -name "*.abc" -exec sh -c 'mv -- "$1" "$(dirname "$1")/$(basename "$1" .abc).edefg"' _ '{}' \;
Solution 5
# Rename all *.txt to *.text
for f in *.txt; do
mv -- "$f" "${f%.txt}.text"
done
Also see the entry on why you shouldn't parse ls
.
Edit: if you have to use basename your syntax would be:
for f in *.txt; do
mv -- "$f" "$(basename "$f" .txt).text"
done
https://unix.stackexchange.com/questions/19654/changing-extension-to-multiple-files
Related videos on Youtube
tommyk
Updated on September 18, 2022Comments
-
tommyk over 1 year
I have many files with
.abc
extension and want to change them to.edefg
How to do this from command line ?I have a root folder with many sub-folders, so the solution should work recursively.
-
Rafał Cieślak about 13 yearsGreat thanks Adam for giving me a tip on how to use *.abc in folders recursively!
-
Eliah Kagan over 11 yearsWelcome to Ask Ubuntu! While this is a valuable answer, I recommend expanding it (by editing) to explain how and why that command works.
-
Anto about 11 yearsGreat tip, thanks ! Where can I find more documentation about the little piece of regex's syntax ? Like what's the
s
at the beginning, and what other options can I use in there. Thanks ! -
user2757729 almost 10 yearsthe first one simply appends the new extension onto the old one, it doesn't replace...
-
geirha almost 10 years@user2757729, it does replace it.
"${1%.abc}"
expands to the value of$1
except without.abc
at the end. See faq 100 for more on shell string manipulations. -
user2757729 almost 10 yearsI ran this on a ~70k files file structure... at least on my shell it most definitely did not replace it.
-
user2757729 almost 10 yearsSorry just set up a test dir structure and it seems to be working. Wonder what I did wrong yesterday.
-
Leon Straathof about 9 years@AdamByrtek I just failed with your suggestion on Utopic. ('no such files' or some such.)
-
geirha almost 9 years@pqnet,
${1%.abc}
is defined by POSIX, if that's what you're referring to. -
rubenscf2 over 8 yearsThis should be the accepted answer. The first line worked great on Ubuntu 14.04, and I was able to add an additional command in there to rename all the files in my fossil repo at the same time (e.g. '<your command' && mv ...').
-
KhoPhi about 8 years@muru but the principle still exist. Simply select any extension then specify the destination extension to get them renamed. The
.3gp
or.mp4
here were just for illustration purposes. -
kvnn about 8 years@user2757729 You probably left the "abc" in
${1%.abc}...
-
TaoPR about 8 yearsThe syntax is preferrable, one-liner and easy to understand. However, I would use
basename "$i" .mp4
to remove the previous extension instead of "ren-$i.mp4". -
KhoPhi about 8 yearsTrue. Good point.
-
spaceghost about 5 yearsIn case anyone else is wondering what the underscore is doing in the first command, it's needed because with
sh -c
the first argument is assigned to $0 and any remaining arguments are assigned to the positional parameters. So the_
is a dummy argument, and '{}' is the first positional argument tosh -c
. -
apollo over 4 yearsThanks for the details explanation, but strangely, when I try this, the
--execdir
did not pass in the leading./
so the\.\/
part in the regex can be omitted. May that is because I am on OSX not ubuntu -
user2364305 over 4 yearsxargs version,
find -iname "*.img" -print0 | xargs -L 1 -I {} -0 sh -c 'mv "{}" "${{}%.img}.iso"'
-
geirha over 4 years@RayFoss that's broken, and attempts to inject the filenames into the shell. Pass the filename as argument instead, then use "$1" to retrieve it inside the script.
...|xargs ... -0 sh -c 'mv "$1" "${1%.img}.iso"' sh {}
-
Fuseteam over 3 years@Anto very late but fwiw and for future reader the
s
at the beginning stands forsubstitute
the/
are the delimiters which can, in theory, be any character as long that character doesn't need to be replaced and the$
mean "at the end of the string" which comes from regex