46

I want to rename files to change their extension, effectively looking to accomplish

mv *.txt *.tsv

But when doing this I get :

*.tsv is not a directory

I find it somewhat strange that the first 10 google hits show mv should work like this.

Evan Carroll
  • 30,763
  • 48
  • 183
  • 315

6 Answers6

49

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
AdminBee
  • 22,803
devnull
  • 5,431
  • 2
    Awesome, thanks! With a bash guide and your explanation I was able to get everything. – user1717828 Jan 27 '15 at 22:51
  • 1
    Works like a charm. Here's another simplified example for file.abc -> blub.xyz in multiple sub dirs: find . -name "file.abc" -exec sh -c 'mv "$1" "$(dirname $1)/blub.xyz"' _ {} \; – Mahn Apr 12 '16 at 15:17
  • how do you know $0 is placeholder? are there other placeholders? @AdminBee – cosmicsage Jan 04 '23 at 08:19
  • @cosmicsage Although you should direct your question at the author of the answer (devnull), you can take a look at the bash manpage in the section ENVIRONMENT. – AdminBee Jan 04 '23 at 12:18
  • this answer works very well when i have to rename 400 .js files to .jsx files in React. just posting my code as an example find . -name "*.js" -exec sh -c 'mv "$1" "${1%.js}.jsx"' _ {} \ – Nivethan Jan 28 '23 at 00:53
31

This answer won't help you change the extension, but it will help you understand why your command is not doing what you expect.

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 is 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 to understand why this won't work. 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 checks to see if the last argument, *.tsv, is a directory. As it is not, the program can not continue as it is not designed to concatenate files. (combine all the files into one.) Nor can it 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 checks 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 end 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 does work on multiple files. But if there are more than two the last one 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. Do you have more links or man pages that you could share where your claim is expressed?

Runium
  • 28,811
9

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.

Hauke Laging
  • 90,279
  • 1
    http://www.cyberciti.biz/tips/renaming-multiple-files-at-a-shell-prompt.html The first hit on google is saying that mv should work as well thats why i found it so strange – Sander Van der Zeeuw Jan 26 '15 at 15:12
  • 3
    @SanderVanderZeeuw I don't know what you read there. The examples use rename, not mv. – Hauke Laging Jan 26 '15 at 15:15
  • 1
    sorry i pasted the wrong HTML. This was the one https://www.udemy.com/blog/rename-a-file-in-linux/ – Sander Van der Zeeuw Jan 27 '15 at 14:00
  • 3
    @SanderVanderZeeuw Embarrassing. These people offer courses? Unfortunately I don't see a contact possibility there. You can easily check whether this works. But in mv *.txt *.tsv mv does (usually) not see *.txt or *.tsv but the shell-expanded file names. The number of files these wildcards expand to would be "random". The only situation where this works is if there is a file with the name *.txt which shall be renamed to (literally) *.tsv (without quoting the bash option nullglob must not be set). – Hauke Laging Jan 27 '15 at 15:29
  • 1
    Or worse. If one have one file named e.g. foo.txt and one named baz.tsv the mv *.txt *.tsv will overwrite the existing .tsv file ... – Runium Jan 27 '15 at 17:38
  • @HaukeLaging Indeed unbelievable that this can happen especially if they provide courses. This is the entire reason i opened this question just to see all of your opinions on this. – Sander Van der Zeeuw Jan 28 '15 at 14:28
6

rename(1)

rename is a perl script by Larry Wall the maker of perl. It takes a Perl regex and operates on the file name.

rename 's/\.txt$/.tsv/' *.txt

Installation

Debian/Ubuntu

If you need to install rename on Debian/Ubuntu you can do

sudo apt install rename
Evan Carroll
  • 30,763
  • 48
  • 183
  • 315
  • This worked fine for me in a folder with 20 000 files, but in a folder with 80 000 files, the "argument list too long" error appeared so I had to find another way. – user985366 May 07 '20 at 12:26
  • 1
    @user985366 one way to get around that is to use xargs. find . -type f -name '*.txt' | xargs rename 's/\.txt$/.tsv/' – Evan Carroll Feb 06 '22 at 18:44
4

For example, if you have asd.txt and qwe.txt files in the directory when you run the command mv *.txt *.tsv, it tries to move these two files into a directory named *.tsv.  Because there is no such directory, it gives an error.

Esref
  • 553
  • 2
  • 6
  • 17
-1

Apparently the easiest way under Linux OS is :

manual rename

  1. Click on file
  2. Press F2 (rename)
  3. alter the file name as you like
  4. press <ENTER>

I guess sometimes DOS does win. :)

raddevus
  • 123
  • (1) Your answer is unclear.  I guess you mean to go to each file with the specified “old” extension (finding them all manually) and then rename each one (manually).  Your presentation is particularly unclear in that you don’t show renaming file1.txt to file1.tsv.  (2) It’s implicit in the question (and in the other answers) that the objective is to find *all* the files that have the specified “old” extension *automatically* and then rename them all to have the same name but with the new extension, and do this all in one operation … (Cont’d) – G-Man Says 'Reinstate Monica' Feb 10 '20 at 06:03
  • (Cont’d) …  (compared to your answer, which would require 987 actions if you have 987 files). – G-Man Says 'Reinstate Monica' Feb 10 '20 at 06:03