I had a set of files that have a common naming scheme and I wantd to replace a word common to all the filenames with another word. Unix-style shell utilities make it easy to do this sort of batch rename operation by finding and replacing patterns in filenames. Windows users can gain access to these powerful programs by installing Cygwin. In this short tutorial I will explain how you can do a batch rename of all files in the current directory by replacing a specific common word in the filenames with another word. I have only tested this on Windows with Cygwin, but it should be very similar on Linux or Unix.
If you want to just replace the file name extension, then see my tutorial on batch renaming file extensions.
I had a number of pictures from my digital camera that are named like so:
IMG_2378.JPG IMG_2379.JPG IMG_2380.JPG
I wanted to do a batch rename to replace “IMG_” with “Fireworks”:
Fireworks2378.JPG Fireworks2379.JPG Fireworks2380.JPG
To perform a batch rename, the basic command looks like this:
for filename in *foo*; do echo mv \"$filename\" \"${filename//foo/bar}\"; done > rename.txt
The command works as follows:
mv “filename” “newfilename”
, where the filename and new file name are surrounded by double quotes (to account for spaces in the file name) and the new file name has all instances of foo
replaced with bar
. The substitution function ${filename//foo/bar}
has two slashes (//
) to replace every occurrence of foo
with bar
.
If you are confident that the command did not mess anything up, then you can directly pipe it directly to /bin/bash
:
for filename in *foo*; do echo mv \"$filename\" \"${filename//foo/bar}\"; done | /bin/bash
Or, you can have Bash execute the mv
command directly (note that the double quotes are no longer escaped with a \
):
for filename in *foo*; do mv "$filename" "${filename//foo/bar}"; done
I do not recommend using either of these direct execution options without checking the output first.
If no files are found matching the *foo*, the loop still executes but the generated command is wrong. Therefore, always check the output before using it.
The image renaming example at the beginning of this page can be now be solved. In the following code blocks, the command after the $ character is what I typed into the Bash prompt. First, I can list the files in the directory:
$ ls -1 IMG_* IMG_2378.JPG IMG_2379.JPG IMG_2380.JPG
Next, I can modify the batch renaming command for this renaming job:
$ for filename in IMG_*; do echo mv \"$filename\" \"${filename//IMG_/Fireworks}\"; done mv "IMG_2378.JPG" "Fireworks2378.JPG" mv "IMG_2379.JPG" "Fireworks2379.JPG" mv "IMG_2380.JPG" "Fireworks2380.JPG"
Finally, by piping the above output into /bin/bash
, the commands can be executed and the files are renamed.
Here are some altnative methods that I have used in the past, each with a few weaknesses that need to be taken into account. I want to document them here because they are instructive.
This was my first workable attempt at doing batch renaming:
ls -1 *foo* | awk '{print("mv "$1 " " $1)}' | sed 's/foo/bar/2' > rename.txt
The command works as follows:
ls -1 *foo*
lists all the files in the current directory with foo
in the file name. It lists one filename per line.awk '{print("mv "$1 " " $1)}'
command. This produces new output where each line is mv FILENAME FILENAME
, with FILENAME being the corresponding filename.awk
command is piped to sed 's/foo/bar/2'
, which replaces the second instance of foo
in a line with bar
. The second instance of foo
corresponds to the second FILENAME in the mv FILENAME FILENAME
lines generated by the awk command. This creates output of the form mv FILENAME NEWFILENAME
, where the new filename is desired filename with foo
replaced with bar
./bin/bash
but this is not recommended.
This technique has some caveats. The first is that since it does not work with file names with spaces. The awk
command prints the first field $1
in each line of input, and the spaces in the file name causes the first word of the filename to be treated as the entire first field.
The second is that since sed
replaces the second instance of foo
in a line, it does not handle the case where the file name had multiple instances of foo
in the first place. For example, if you had a file named “foofoo.jpg”, then the corresponding command generated by the above code would be:
mv foobar.jpg foofoo.jpg
The second foo
is replaced with bar
, which is not what you want. Thus, this technique does not work when there is more than one instance of the word you want to replace in the filename.
An alternative I tried to get around the limitation in the previous technique of only one instance of the search term being allowed to occur per file name:
paste <(ls -1 *foo* | sed 's/^/mv "/;s/$/"/') <(ls -1 *foo* | sed 's/^/"/;s/$/"/;s/foo/bar/g') -d ' ' > rename.txt
How it works:
<( … )
are processed first. This is a process substitution in Bash and the output of the command inside is given to paste
.*foo*
and sed
prepends mv ”
and appends a double quote to the end. This surrounds the file name with double quotes to take care of spaces.*foo*
and then uses sed
to surround the file name with double quotes and replace “foo” with “bar” in the file name.paste
command concatenates the output of the two process substitutions line by line, and uses a space as a delimiter. This produces a list of mv
commands that can be saved to a file or piped to /bin/bash for execution.I do not see too many weaknesses in this technique but it seems cryptic and perhaps inelegant.
Discussion
Thanks for your example of renaming files. I only wanted to rename some music files. I found the fancy sed commands confusing and I found your simple suggestion (the first one) worked well. I especially appreciated how one can check this first with rename.txt, before renaming the files.
Rose
for filename in *foo*; do echo mv \"$filename\" \"`echo "$filename"|awk '{print toupper($filename)}'`\"; done
cmd /C rename \*foo\* \*bar\*
Note "cmd /C [windows_cmd_command]" makes cmd execute the cmd command that follows "/C," and that you need to escape the asterisks ("\*") to prevent your cygwin shell (bash by default) from doing the globbing before sending the command to cmd.
e.g. `mv: cannot stat `"454Reads.HAL3W_2_L.sff"': No such file or directory`.
The `awk` and `sed` methods work great.
Thanks
On windows by cmd command dos, go in directory and type:
rename winter*. summer*.*
rename IMG_ Fireworks *
Syntax: rename <Characters You Don't Want> <Characters You Do Want> filenames (wildcards are acceptable)
Many thanks for the rename script it was well documented and worked. Short and simple.
Great job.
Terry
I have files that are named with our state ID number that I want to rename with out school ID number. I have a csv file of state number with school number in next column. How can I rename the files like this:
04459.jpg to 1234567.jpg
0562.jpg to 145874.jpg
etc based on my file.
I am trying to copy every single image and video on my Mac to a single folder for backup purposes. Here are a couple of lines I am currently using in my bash script.
find /Users/"$(whoami)"/Library/Messages/Attachments -name "*.jpg" -exec cp {} /Users/"$(whoami)"/Desktop/ImagesBackup/$now \;;
find /Users/"$(whoami)"/Library/Messages/Attachments -name "*.JPG" -exec cp {} /Users/"$(whoami)"/Desktop/ImagesBackup/$now \;;
find /Users/"$(whoami)"/Library/Messages/Attachments -name "*.png" -exec cp {} /Users/"$(whoami)"/Desktop/ImagesBackup/$now \;;
find /Users/"$(whoami)"/Library/Messages/Attachments -name "*.PNG" -exec cp {} /Users/"$(whoami)"/Desktop/ImagesBackup/$now \;;
The problem I am running in to is duplicates. How can I have my script rename every single file as it is copied into the new location?
Thanks,
John
#!/bin/bash
for filename in *"$1"*; do /bin/mv "$filename" "${filename//$2/$3}"; done
(The command goes all on one line, the site entered a carriage return when I pasted it.) Make it executable according to your OS (I did this on macOS 10.13.4) and type,
rename IMG_ IMG_ Fireworks
Example : FILE1.222.M.001, FILE2.222.M.002 etc
Can someone help with the awk statement to rename the file as follows FILE1.222.Z.001, FILE2.222.Z.002
I am trying something like this but its not working
ls -l *222.M* | awk '{print("mv "$1 " " $1)}' | sed 's/M/Z/2'
(replacing M with Z )