How do I change extension of multiple files recursively from the command line?

145,963

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 of find. 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" part

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

Share:
145,963

Related videos on Youtube

tommyk
Author by

tommyk

Updated on September 18, 2022

Comments

  • tommyk
    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
    Rafał Cieślak about 13 years
    Great thanks Adam for giving me a tip on how to use *.abc in folders recursively!
  • Eliah Kagan
    Eliah Kagan over 11 years
    Welcome to Ask Ubuntu! While this is a valuable answer, I recommend expanding it (by editing) to explain how and why that command works.
  • Anto
    Anto about 11 years
    Great 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
    user2757729 almost 10 years
    the first one simply appends the new extension onto the old one, it doesn't replace...
  • geirha
    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
    user2757729 almost 10 years
    I ran this on a ~70k files file structure... at least on my shell it most definitely did not replace it.
  • user2757729
    user2757729 almost 10 years
    Sorry just set up a test dir structure and it seems to be working. Wonder what I did wrong yesterday.
  • Leon Straathof
    Leon Straathof about 9 years
    @AdamByrtek I just failed with your suggestion on Utopic. ('no such files' or some such.)
  • geirha
    geirha almost 9 years
    @pqnet, ${1%.abc} is defined by POSIX, if that's what you're referring to.
  • rubenscf2
    rubenscf2 over 8 years
    This 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
    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
    kvnn about 8 years
    @user2757729 You probably left the "abc" in ${1%.abc}...
  • TaoPR
    TaoPR about 8 years
    The 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
    KhoPhi about 8 years
    True. Good point.
  • spaceghost
    spaceghost about 5 years
    In 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 to sh -c.
  • apollo
    apollo over 4 years
    Thanks 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
    user2364305 over 4 years
    xargs version, find -iname "*.img" -print0 | xargs -L 1 -I {} -0 sh -c 'mv "{}" "${{}%.img}.iso"'
  • geirha
    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
    Fuseteam over 3 years
    @Anto very late but fwiw and for future reader the s at the beginning stands for substitute 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