How do I change extension of multiple files recursively from the command line?
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.
9 Answers 9
A portable way (which will work on any POSIX compliant system):
find /the/path -depth -name "*.abc" -exec sh -c 'mv "$1" "$.edefg"' _ <> \;
In bash4, you can use globstar to get recursive globs (**):
shopt -s globstar for file in /the/path/**/*.abc; do mv "$file" "$.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 < $; i += 100)); do rename 's/\.abc$/.edefg/' "$" done # Using find: find /the/path -depth -name "*.abc" -exec rename 's/\.abc$/.edefg/' <> +
@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.1%.abc>
I ran this on a ~70k files file structure. at least on my shell it most definitely did not replace it.
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 .
This will do the required task if all the files are in the same folder
To rename the files recursively use this:
find /path/to/root/folder -type f -name '*.abc' -print0 | xargs -0 rename 's/.abc$/.edefg/'
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 !
@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
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/' '<>' \;
- -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!
- 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
tree --charset=ascii |-- a_file.abc |-- Another.abc |-- Not_this.def `-- dir1 `-- nested_file.abc
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.
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
find /the/path -depth -type f -name "*.abc" -exec sh -c 'mv -- "$1" "$(dirname "$1")/$(basename "$1" .abc).edefg"' _ '<>' \;
Welcome to Ask Ubuntu! While this is a valuable answer, I recommend expanding it (by editing) to explain how and why that command works.
# Rename all *.txt to *.text for f in *.txt; do mv -- "$f" "$.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
The ; matches zero or more */ and corresponds to #1 in the replacement. The * corresponds to #2 . The non-recursive version would be
This is what I did and worked pretty just the way I wanted. I used the mv command. I had multiple .3gp files and I wanted to rename them all to .mp4
Here’s a short oneliner for it:
for i in *.3gp; do mv -- "$i" "ren-$i.mp4"; done
Which simply scans through the current directory, picks up all .3gp files, then renames (using the mv) into ren-name_of_file.mp4
@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.
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».
I found an easy way to achieve this. To change extensions of many files from jpg to pdf, use:
for file in /path/to; do mv $file $(basename -s jpg $file)pdf ; done
Rename files and directories with find -execdir | rename
If you are going to rename both files and directories not simply with a suffix, then this is a good pattern:
PATH="$(echo "$PATH" | sed -E 's/(^|:)[^\/][^:]*//g')" \ find . -depth -execdir rename 's/findme/replaceme/' '<>' \;
The awesome -execdir option does a cd into the directory before executing the rename command, unlike -exec .
-depth ensure that the renaming happens first on children, and then on parents, to prevent potential problems with missing parent directories.
-execdir is required because rename does not play well with non-basename input paths, e.g. the following fails:
rename 's/findme/replaceme/g' acc/acc
The PATH hacking is required because -execdir has one very annoying drawback: find is extremely opinionated and refuses to do anything with -execdir if you have any relative paths in your PATH environment variable, e.g. ./node_modules/.bin , failing with:
find: The relative path ‘./node_modules/.bin’ is included in the PATH environment variable, which is insecure in combination with the -execdir action of find. Please remove that entry from $PATH
-execdir is a GNU find extension to POSIX. rename is Perl based and comes from the rename package. Tested in Ubuntu 18.10.
Rename lookahead workaround
If your input paths don’t come from find , or if you’ve had enough of the relative path annoyance, we can use some Perl lookahead to safely rename directories as in:
git ls-files | sort -r | xargs rename 's/findme(. *\/)\/?$/replaceme/g' '<>'
The sort -r is required to ensure that files come after their respective directories, since longer paths come after shorter ones with the same prefix.
How to use sed to change file extensions?
I have to do a sed line (also using pipes in Linux) to change a file extension, so I can do some kind of mv *.1stextension *.2ndextension like mv *.txt *.c . The thing is that I can’t use batch or a for loop, so I have to do it all with pipes and sed command.
7 Answers 7
you can use string manipulation
Or if your system support, you can use rename .
you can also do something like this
which is called brace expansion
what exactly your input? what kind of output do you expect? if only sed allowed I don’t think you can rename a file.
@user2113403: for filename in *.1stextension; do mv «$
sed is for manipulating the contents of files, not the filename itself. My suggestion:
Or, there’s this existing question which should help.
find . -name "*.txt" | sed -e 's|./||g' | awk '' | sed -e "s|\.txtc|\.c|g" > table; chmod u+x table; ./table
I don’t know why you can’t use a loop. It makes life much easier :
newex="c"; # Give your new extension for file in *.*; # You can replace with *.txt instead of *.* do ex="$"; # This retrieves the file extension ne=$(echo "$file" | sed -e "s|$ex|$newex|g"); # Replaces current with the new one echo "$ex";echo "$ne"; mv "$file" "$ne"; done
You can use find to find all of the files and then pipe that into a while read loop:
$ find . -name "*.ext1" -print0 | while read -d $'\0' file do mv $file "$.ext2" done
The $ is the small right pattern filter. The % marks the pattern to remove from the right side (matching the smallest glob pattern possible), The .* is the pattern (the last . followed by the characters after the . ).
The -print0 will separate file names with the NUL character instead of \n . The -d $’\0′ will read in file names separated by the NUL character. This way, file names with spaces, tabs, \n , or other wacky characters will be processed correctly.
Recursively change file extensions in Bash
I want to recursively iterate through a directory and change the extension of all files of a certain extension, say .t1 to .t2 . What is the bash command for doing this?
@AmalAntony : If you don’t have rename , write a shell script, which renames a single file (trivial to do in your simple case), and then use find to apply this script to all files with the offending extension.
6 Answers 6
find . -name "*.t1" -exec bash -c 'mv "$1" "$".t2' - '<>' +
If you have rename available then use one of these:
find . -name '*.t1' -exec rename .t1 .t2 <> +
find . -name "*.t1" -exec rename 's/\.t1$/.t2/' '<>' +
(My version of rename doesn’t allow the sed style substitution expression. Gotta love Linux. I used to have to install TotalCommander for Windows to do stuff like this.)
In case anyone is wondering what the «$<1%.t1>«.t2 part does, like I did: It uses bash string manipulation to do the following: 1/ Take the first positional parameter $1 and truncate the .t1 string literal from its end (percentage sign % operator). 2/ Append the .t2 string literal to the result.1%.t1>
Delimiter argument should be ; instead of + if renaming all at once is required like this find . -name «*.t1» -exec bash -c ‘mv «$1» «$<1%.t1>«.t2′ — ‘<>‘ \; . Otherwise with the + only one file will be renamed at a time. Ref 1%.t1>
None of the suggested solutions worked for me on a fresh install of debian 11. This should work on any Posix/MacOS
find ./ -depth -name "*.t1" -exec sh -c 'mv "$1" "$.t2"' _ <> \;
This is the only one worked for me. None of the accepted answer methods worked. I am on Ubuntu so probably a Debian/CentOs whatever thing. cheers
If your version of bash supports the globstar option (version 4 or later):
shopt -s globstar for f in **/*.t1; do mv "$f" "$.t2" done
I would do this way in bash :
for i in $(ls *.t1); do mv "$i" "$.t2" done
EDIT : my mistake : it’s not recursive, here is my way for recursive changing filename :
for i in $(find `pwd` -name "*.t1"); do mv "$i" "$.t2" done
Don’t parse ls, and see the same page for why your find syntax is bad. Also, make sure you quote your variables
Or you can simply install the mmv command and do:
Here #1 is the first glob part i.e. the * in *.t1 .
Or in pure bash stuff, a simple way would be:
for f in *.t1; do mv "$f" "$.t2" done
(i.e.: for can list files without the help of an external command such as ls or find )
I assume the OP’s use of «recursively» refers to renaming files in subdirectories of the directory as well.
My lazy copy-pasting of one of these solutions didn’t work, but I already had fd-find installed, so I used that:
fd --extension t1 --exec mv <> .t2
From fd ‘s manpage, when executing a command (using —exec ):
The following placeholders are substituted by a path derived from the current search result: <> path >basename/> parent directory <.>path without file extension basename without file extension