Rename multiple files with mv to change the extension

I know this doesn't answer your question, but in case you were looking for another way to rename the files compared to your work-around loop, why not use find? I have used this command many times to replace file extensions in large directories with hundreds of thousands of files in it. This should work on any POSIX-compliant system:

find . -name "*.gappedPeak" -exec sh -c 'mv "$1" "${1%.gappedPeak}.bed"' _ {} \;

Command Breakdown:

  1. '.' => search path starting at current directory marked by ' . '

  2. -name => set find match name (in this case all files that end with .gappedPeak)

  3. -exec => execute the following command on every match

  4. sh -c => 'exec' creates an independent shell environment for each match

  5. mv "$1" "${1%.gappedPeak}.bed" => mv first variable (denoted by $1), which is the current file name, to new name. Here I do a substring match and delete; so take first var again, $1 and use % to delete .gappedPeak from the string. The .bed at the end just concatenates the remaining variable, which in the example below would now be testNumber, with .bed, creating the new testNumber.bed filename.

  6. The underscore is a placeholder for $0

  7. The {} is replaced by each (*.gappedPeak) filename found by the find command, and becomes $1 to the sh command.

  8. \; marks the end of the -exec command.  You can also use ';' or ";".

Example:

[user@before]# ls -lh
total 0
-rw-r--r--. 1 root root 0 Jan 26 11:40 test1.gappedPeak
-rw-r--r--. 1 root root 0 Jan 26 11:40 test2.gappedPeak
-rw-r--r--. 1 root root 0 Jan 26 11:40 test3.gappedPeak
-rw-r--r--. 1 root root 0 Jan 26 11:40 test4.gappedPeak
-rw-r--r--. 1 root root 0 Jan 26 11:40 test5.gappedPeak

[user@after]# ls -lh
total 0
-rw-r--r--. 1 root root 0 Jan 26 11:40 test1.bed
-rw-r--r--. 1 root root 0 Jan 26 11:40 test2.bed
-rw-r--r--. 1 root root 0 Jan 26 11:40 test3.bed
-rw-r--r--. 1 root root 0 Jan 26 11:40 test4.bed
-rw-r--r--. 1 root root 0 Jan 26 11:40 test5.bed

When you issue the command:

mv *.txt *.tsv

the shell, lets assume bash, expands the wildcards if there are any matching files (including directories). The list of files are passed to the program, here mv. If no matches are found the unexpanded version is passed.

Again: the shell expands the patterns, not the program.


Loads of examples is perhaps best way, so here we go:

Example 1:

$ ls
file1.txt file2.txt

$ mv *.txt *.tsv

Now what happens on the mv line is that the shell expands *.txt to the matching files. As there are no *.tsv files that is not changed.

The mv command is called with two special arguments:

  • argc: Number of arguments, including the program.
  • argv: An array of arguments, including the program as first entry.

In the above example that would be:

 argc = 4
 argv[0] = mv
 argv[1] = file1.txt
 argv[2] = file2.txt
 argv[3] = *.tsv

The mv program check to see if last argument, *.tsv, is a directory. As it is not, the program can not continue as it is not designed to concatenate files. (Typically move all the files into one.) Nor create directories on a whim.

As a result it aborts and reports the error:

mv: target ‘*.tsv’ is not a directory

Example 2:

Now if you instead say:

$ mv *1.txt *.tsv

The mv command is executed with:

 argc = 3
 argv[0] = mv
 argv[1] = file1.txt
 argv[2] = *.tsv

Now, again, mv check to see if *.tsv exists. As it does not the file file1.txt is moved to *.tsv. That is: the file is renamed to *.tsv with the asterisk and all.

$ mv *1.txt *.tsv
‘file1.txt’ -> ‘*.tsv’

$ ls
file2.txt *.tsv

Example 3:

If you instead said:

$ mkdir *.tsv
$ mv *.txt *.tsv

The mv command is executed with:

 argc = 3
 argv[0] = mv
 argv[1] = file1.txt
 argv[1] = file2.txt
 argv[2] = *.tsv

As *.tsv now is a directory, the files ends up being moved there.


Now: using commands like some_command *.tsv when the intention is to actually keep the wildcard one should always quote it. By quoting you prevent the wildcards from being expanded if there should be any matches. E.g. say mkdir "*.tsv".

Example 4:

The expansion can further be viewed if you do for example:

$ ls
file1.txt file2.txt

$ mkdir *.txt
mkdir: cannot create directory ‘file1.txt’: File exists
mkdir: cannot create directory ‘file2.txt’: File exists

Example 5:

Now: the mv command can and do work on multiple files. But if there is more then two the last has to be a target directory. (Optionally you can use the -t TARGET_DIR option, at least for GNU mv.)

So this is OK:

$ ls -F
b1.tsv  b2.tsv  f1.txt  f2.txt  f3.txt  foo/

$ mv *.txt *.tsv foo

Here mv would be called with:

 argc = 7
 argv[0] = mv
 argv[1] = b1.tsv
 argv[2] = b2.tsv
 argv[3] = f1.txt
 argv[4] = f2.txt
 argv[5] = f3.txt
 argv[6] = foo

and all the files end up in the directory foo.


As for your links. You have provided one (in a comment), where mv is not mentioned at all, but rename. If you have more links you could share. As well as for man pages where you claim this is expressed.


mv *.txt *.tsv doesn't work; mv can rename only one file at a time. You have either misunderstood the explanations or they are wrong.

mmv and rename can rename several files at once. But there are two versions of rename around which are called differently. There should be plenty of questions about that here.

Tags:

Rename

Mv