6

I found this answer how do i... but it simply doesn't work - it did not rename any file for unknown to me reason

Before I started to search around I thought that it should be easy task even for novice penguin, but it doesn't seem so for me.

For example, I simply can't tell ls to list all *.txt in all subfolders, which was surprise to me (without grep or similar).
Then I found find and find . -name name_1.txt lists files fine, but

for f in $(find . -name name_1.txt) ; do echo "$f" ; done

splits whole file paths with space as separator, so it's unusable to pass that output to some command like mv or rename

I want to ask whats wrong with above command and if possible some nifty oneliner so I can recursively rename name_1.txt to name_2.txt

4
  • What's the error you got when executing the command found in the other answer? Does any of your filenames contain a space in it?
    – jcollado
    Nov 18, 2011 at 12:45
  • I don't get error, but none file was renamed. Filename does not have spaces but subfolder names do. It's on Windows NTFS partition but that shouldn't change things I guess
    – zetah
    Nov 18, 2011 at 12:55
  • 1
    I believe that's the reason it didn't work for you. The answer from @ams with find -print0 and xargs -0 uses the null character as a separator to avoid whitespace problems in the file names.
    – jcollado
    Nov 18, 2011 at 13:48

4 Answers 4

12

find . -name '*.txt' -print0 | xargs -0 -n1 bash -c 'mv "$0" "${0/oldname/newname}"'

Obviously, that rename pattern is just a simple example, but beware that as is it will edit the whole path, not just the filename.

4
  • Thanks for you answer. As I don't get xargs logic (and your warning), could you just edit "${0/oldname/newname}" part with case where I want to rename all files named name_1.txt to name_2.txt recursively. Should it be "${0/name_1.txt/name_2.txt}"?
    – zetah
    Nov 18, 2011 at 12:57
  • I tried is on sample folder and it works as assumed in my previous comment
    – zetah
    Nov 18, 2011 at 13:15
  • 1
    Let me break it down: print0 makes find output zero-terminated strings, so spaces are safe; -0 makes xargs expect zero-terminated input; -n1 says to invoke bash once for each file; and ${0/old/new} is bash syntax for find and replace in variable $0. Hope that helps.
    – ams
    Nov 18, 2011 at 15:14
  • It's sure interesting :) and new to me, and usable in future terminal sessions
    – zetah
    Nov 18, 2011 at 15:31
4

the mmv command will do this in a rather simple call:

mmv ";name_1.txt" "#1name_2.txt"

The ";" wildcard, which is reused in the replacement filename as "#1" matches also subdirectories.

If you need more complicated examples, you should look into the man-page.

1
  • Thanks, but I didn't tried it as it's currently not installed on my 11.04 and first answer worked fine. Cheers
    – zetah
    Nov 18, 2011 at 13:17
4

As @ams points out, many recursive replace solutions require you to work with the whole path, not just the file name.

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 replace command.

find /the/path -type f \
               -name '*_one.txt' \
               -execdir rename 's/\.\/(.+)_one\.txt$/$1_two.txt/' {} \;

In detail:

  • -type f means only look for files, not directories
  • -name '*_one.txt' means means only match filenames that end in _one.txt
  • In this answer, I'm using the rename command instead of mv. rename uses a Perl regular expression to rename each file.
  • 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 in practice.
  • However, the backslash at the end of the -execdir line is required. It is there to esacpe 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
  • (.+) match the start of the filename. The parentheses capture the match for later use
  • _one\.txt match "_one.txt", escape the dot metacharacter
  • $ 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
  • $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.
  • _two.txt the new file name will end in "_two.txt". No need to escape the dot metacharacter here in the "replace" section
  • / end of the regex

Before

tree --charset=ascii

|-- a_one.txt
|-- b_one.txt
|-- Not_this.txt
`-- dir1
    `-- c_one.txt

After

tree --charset=ascii

|-- a_two.txt
|-- b_two.txt
|-- Not_this.txt
`-- dir1
    `-- c_two.txt

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.

1
  • 1
    execdir is good, by why not just find ~/the/path -type f -name '*_one.txt' -execdir bash -c 'mv -- "$0" "${0%one.txt}two.txt"' {} \;? It is easier to read and memorize.
    – emk2203
    Jan 14, 2019 at 12:38
0

You could explore the plethora of choices available:

`$ apt-cache search rename | grep -i rename` 

Not the answer you're looking for? Browse other questions tagged .